Merge branch 'main' into za/1602-extend-expirations-easily

This commit is contained in:
zandercymatics 2024-02-22 09:55:33 -07:00
commit ca43d91641
No known key found for this signature in database
GPG key ID: FF4636ABEC9682B7
68 changed files with 5306 additions and 4589 deletions

View file

@ -37,3 +37,11 @@ jobs:
cf_org: cisa-dotgov cf_org: cisa-dotgov
cf_space: stable cf_space: stable
cf_manifest: "ops/manifests/manifest-stable.yaml" cf_manifest: "ops/manifests/manifest-stable.yaml"
- name: Run Django migrations
uses: cloud-gov/cg-cli-tools@main
with:
cf_username: ${{ secrets.CF_STABLE_USERNAME }}
cf_password: ${{ secrets.CF_STABLE_PASSWORD }}
cf_org: cisa-dotgov
cf_space: stable
cf_command: "run-task getgov-stable --command 'python manage.py migrate' --name migrate"

View file

@ -37,3 +37,11 @@ jobs:
cf_org: cisa-dotgov cf_org: cisa-dotgov
cf_space: staging cf_space: staging
cf_manifest: "ops/manifests/manifest-staging.yaml" cf_manifest: "ops/manifests/manifest-staging.yaml"
- name: Run Django migrations
uses: cloud-gov/cg-cli-tools@main
with:
cf_username: ${{ secrets.CF_STAGING_USERNAME }}
cf_password: ${{ secrets.CF_STAGING_PASSWORD }}
cf_org: cisa-dotgov
cf_space: staging
cf_command: "run-task getgov-staging --command 'python manage.py migrate' --name migrate"

View file

@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi" name = "pypi"
[packages] [packages]
django = "*" django = "4.2.10"
cfenv = "*" cfenv = "*"
django-cors-headers = "*" django-cors-headers = "*"
pycryptodomex = "*" pycryptodomex = "*"

1137
src/Pipfile.lock generated

File diff suppressed because it is too large Load diff

View file

@ -19,7 +19,6 @@ API_BASE_PATH = "/api/v1/available/?domain="
class AvailableViewTest(MockEppLib): class AvailableViewTest(MockEppLib):
"""Test that the view function works as expected.""" """Test that the view function works as expected."""
def setUp(self): def setUp(self):
@ -123,7 +122,6 @@ class AvailableViewTest(MockEppLib):
class AvailableAPITest(MockEppLib): class AvailableAPITest(MockEppLib):
"""Test that the API can be called as expected.""" """Test that the API can be called as expected."""
def setUp(self): def setUp(self):

View file

@ -1,4 +1,5 @@
"""Internal API views""" """Internal API views"""
from django.apps import apps from django.apps import apps
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.http import HttpResponse from django.http import HttpResponse

View file

@ -4,13 +4,13 @@ from django.http import HttpResponse
from django.test import Client, TestCase, RequestFactory from django.test import Client, TestCase, RequestFactory
from django.urls import reverse from django.urls import reverse
from djangooidc.exceptions import NoStateDefined from djangooidc.exceptions import NoStateDefined, InternalError
from ..views import login_callback from ..views import login_callback
from .common import less_console_noise from .common import less_console_noise
@patch("djangooidc.views.CLIENT", autospec=True) @patch("djangooidc.views.CLIENT", new_callable=MagicMock)
class ViewsTest(TestCase): class ViewsTest(TestCase):
def setUp(self): def setUp(self):
self.client = Client() self.client = Client()
@ -35,116 +35,253 @@ class ViewsTest(TestCase):
pass pass
def test_openid_sets_next(self, mock_client): def test_openid_sets_next(self, mock_client):
# setup """Test that the openid method properly sets next in the session."""
with less_console_noise():
# SETUP
# set up the callback url that will be tested in assertions against
# session[next]
callback_url = reverse("openid_login_callback") callback_url = reverse("openid_login_callback")
# mock # MOCK
# when login is called, response from create_authn_request should
# be returned to user, so let's mock it and test it
mock_client.create_authn_request.side_effect = self.say_hi mock_client.create_authn_request.side_effect = self.say_hi
# in this case, we need to mock the get_default_acr_value so that
# openid method will execute properly, but the acr_value itself
# is not important for this test
mock_client.get_default_acr_value.side_effect = self.create_acr mock_client.get_default_acr_value.side_effect = self.create_acr
# test # TEST
# test the login url, passing a callback url
response = self.client.get(reverse("login"), {"next": callback_url}) response = self.client.get(reverse("login"), {"next": callback_url})
# assert # ASSERTIONS
session = mock_client.create_authn_request.call_args[0][0] session = mock_client.create_authn_request.call_args[0][0]
# assert the session[next] is set to the callback_url
self.assertEqual(session["next"], callback_url) self.assertEqual(session["next"], callback_url)
# assert that openid returned properly the response from
# create_authn_request
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Hi") self.assertContains(response, "Hi")
def test_openid_raises(self, mock_client): def test_openid_raises(self, mock_client):
# mock """Test that errors in openid raise 500 error for the user.
mock_client.create_authn_request.side_effect = Exception("Test") This test specifically tests for any exceptions that might be raised from
# test create_authn_request. This includes scenarios where CLIENT exists, but
is no longer functioning properly."""
with less_console_noise(): with less_console_noise():
# MOCK
# when login is called, exception thrown from create_authn_request
# should present 500 error page to user
mock_client.create_authn_request.side_effect = Exception("Test")
# TEST
# test when login url is called
response = self.client.get(reverse("login")) response = self.client.get(reverse("login"))
# assert # ASSERTIONS
# assert that the 500 error page is raised
self.assertEqual(response.status_code, 500) self.assertEqual(response.status_code, 500)
self.assertTemplateUsed(response, "500.html") self.assertTemplateUsed(response, "500.html")
self.assertIn("Server error", response.content.decode("utf-8")) self.assertIn("Server error", response.content.decode("utf-8"))
def test_callback_with_no_session_state(self, mock_client): def test_openid_raises_when_client_is_none_and_cant_init(self, mock_client):
"""Test that errors in openid raise 500 error for the user.
This test specifically tests for the condition where the CLIENT
is None and the client initialization attempt raises an exception."""
with less_console_noise():
# MOCK
# mock that CLIENT is None
# mock that Client() raises an exception (by mocking _initialize_client)
# Patch CLIENT to None for this specific test
with patch("djangooidc.views.CLIENT", None):
# Patch _initialize_client() to raise an exception
with patch("djangooidc.views._initialize_client") as mock_init:
mock_init.side_effect = InternalError
# TEST
# test when login url is called
response = self.client.get(reverse("login"))
# ASSERTIONS
# assert that the 500 error page is raised
self.assertEqual(response.status_code, 500)
self.assertTemplateUsed(response, "500.html")
self.assertIn("Server error", response.content.decode("utf-8"))
def test_openid_initializes_client_and_calls_create_authn_request(self, mock_client):
"""Test that openid re-initializes the client when the client had not
been previously initiated."""
with less_console_noise():
# MOCK
# response from create_authn_request should
# be returned to user, so let's mock it and test it
mock_client.create_authn_request.side_effect = self.say_hi
# in this case, we need to mock the get_default_acr_value so that
# openid method will execute properly, but the acr_value itself
# is not important for this test
mock_client.get_default_acr_value.side_effect = self.create_acr
with patch("djangooidc.views._initialize_client") as mock_init_client:
with patch("djangooidc.views._client_is_none") as mock_client_is_none:
# mock the client to initially be None
mock_client_is_none.return_value = True
# TEST
# test when login url is called
response = self.client.get(reverse("login"))
# ASSERTIONS
# assert that _initialize_client was called
mock_init_client.assert_called_once()
# assert that the response is the mocked response from create_authn_request
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Hi")
def test_login_callback_with_no_session_state(self, mock_client):
"""If the local session is None (ie the server restarted while user was logged out), """If the local session is None (ie the server restarted while user was logged out),
we do not throw an exception. Rather, we attempt to login again.""" we do not throw an exception. Rather, we attempt to login again."""
# mock with less_console_noise():
# MOCK
# mock the acr_value to some string
# mock the callback function to raise the NoStateDefined Exception
mock_client.get_default_acr_value.side_effect = self.create_acr mock_client.get_default_acr_value.side_effect = self.create_acr
mock_client.callback.side_effect = NoStateDefined() mock_client.callback.side_effect = NoStateDefined()
# test # TEST
with less_console_noise(): # test the login callback
response = self.client.get(reverse("openid_login_callback")) response = self.client.get(reverse("openid_login_callback"))
# assert # ASSERTIONS
# assert that the user is redirected to the start of the login process
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/") self.assertEqual(response.url, "/")
def test_login_callback_reads_next(self, mock_client): def test_login_callback_reads_next(self, mock_client):
# setup """If the next value is set in the session, test that login_callback returns
a redirect to the 'next' url."""
with less_console_noise():
# SETUP
session = self.client.session session = self.client.session
# set 'next' to the logout url
session["next"] = reverse("logout") session["next"] = reverse("logout")
session.save() session.save()
# mock # MOCK
# mock that callback returns user_info; this is the expected behavior
mock_client.callback.side_effect = self.user_info mock_client.callback.side_effect = self.user_info
# test # patch that the request does not require step up auth
with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): # TEST
# test the login callback url
with patch("djangooidc.views._requires_step_up_auth", return_value=False):
response = self.client.get(reverse("openid_login_callback")) response = self.client.get(reverse("openid_login_callback"))
# assert # ASSERTIONS
# assert the redirect url is the same as the 'next' value set in session
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("logout")) self.assertEqual(response.url, reverse("logout"))
def test_login_callback_no_step_up_auth(self, mock_client): def test_login_callback_raises_when_client_is_none_and_cant_init(self, mock_client):
"""Walk through login_callback when requires_step_up_auth returns False """Test that errors in login_callback raise 500 error for the user.
and assert that we have a redirect to /""" This test specifically tests for the condition where the CLIENT
# setup is None and the client initialization attempt raises an exception."""
with less_console_noise():
# MOCK
# mock that CLIENT is None
# mock that Client() raises an exception (by mocking _initialize_client)
# Patch CLIENT to None for this specific test
with patch("djangooidc.views.CLIENT", None):
# Patch _initialize_client() to raise an exception
with patch("djangooidc.views._initialize_client") as mock_init:
mock_init.side_effect = InternalError
# TEST
# test the login callback url
response = self.client.get(reverse("openid_login_callback"))
# ASSERTIONS
# assert that the 500 error page is raised
self.assertEqual(response.status_code, 500)
self.assertTemplateUsed(response, "500.html")
self.assertIn("Server error", response.content.decode("utf-8"))
def test_login_callback_initializes_client_and_succeeds(self, mock_client):
"""Test that openid re-initializes the client when the client had not
been previously initiated."""
with less_console_noise():
# SETUP
session = self.client.session session = self.client.session
session.save() session.save()
# mock # MOCK
# mock that callback returns user_info; this is the expected behavior
mock_client.callback.side_effect = self.user_info mock_client.callback.side_effect = self.user_info
# test # patch that the request does not require step up auth
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):
with patch("djangooidc.views._initialize_client") as mock_init_client:
with patch("djangooidc.views._client_is_none") as mock_client_is_none:
# mock the client to initially be None
mock_client_is_none.return_value = True
# TEST
# test the login callback url
response = self.client.get(reverse("openid_login_callback")) response = self.client.get(reverse("openid_login_callback"))
# assert # ASSERTIONS
# assert that _initialize_client was called
mock_init_client.assert_called_once()
# assert that redirect is to / when no 'next' is set
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/") self.assertEqual(response.url, "/")
def test_requires_step_up_auth(self, mock_client): def test_login_callback_no_step_up_auth(self, mock_client):
"""Invoke login_callback passing it a request when requires_step_up_auth returns True """Walk through login_callback when _requires_step_up_auth returns False
and assert that we have a redirect to /"""
with less_console_noise():
# SETUP
session = self.client.session
session.save()
# MOCK
# mock that callback returns user_info; this is the expected behavior
mock_client.callback.side_effect = self.user_info
# patch that the request does not require step up auth
# TEST
# test the login callback url
with patch("djangooidc.views._requires_step_up_auth", return_value=False):
response = self.client.get(reverse("openid_login_callback"))
# ASSERTIONS
# assert that redirect is to / when no 'next' is set
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/")
def test_login_callback_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.""" and assert that session is updated and create_authn_request (mock) is called."""
with less_console_noise():
# MOCK
# Configure the mock to return an expected value for get_step_up_acr_value # 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" mock_client.return_value.get_step_up_acr_value.return_value = "step_up_acr_value"
# Create a mock request # Create a mock request
request = self.factory.get("/some-url") request = self.factory.get("/some-url")
request.session = {"acr_value": ""} request.session = {"acr_value": ""}
# Ensure that the CLIENT instance used in login_callback is the mock # Ensure that the CLIENT instance used in login_callback is the mock
# patch requires_step_up_auth to return True # patch _requires_step_up_auth to return True
with patch("djangooidc.views.requires_step_up_auth", return_value=True), patch( with patch("djangooidc.views._requires_step_up_auth", return_value=True), patch(
"djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock() "djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock()
) as mock_create_authn_request: ) as mock_create_authn_request:
# TEST
# test the login callback
login_callback(request) login_callback(request)
# ASSERTIONS
# create_authn_request only gets called when requires_step_up_auth is True # create_authn_request only gets called when _requires_step_up_auth is True
# and it changes this acr_value in request.session # and it changes this acr_value in request.session
# Assert that acr_value is no longer empty string # Assert that acr_value is no longer empty string
self.assertNotEqual(request.session["acr_value"], "") self.assertNotEqual(request.session["acr_value"], "")
# And create_authn_request was called again # And create_authn_request was called again
mock_create_authn_request.assert_called_once() mock_create_authn_request.assert_called_once()
def test_does_not_requires_step_up_auth(self, mock_client): def test_login_callback_does_not_requires_step_up_auth(self, mock_client):
"""Invoke login_callback passing it a request when requires_step_up_auth returns False """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. 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""" Possibly redundant with test_login_callback_requires_step_up_auth"""
with less_console_noise():
# MOCK
# Create a mock request # Create a mock request
request = self.factory.get("/some-url") request = self.factory.get("/some-url")
request.session = {"acr_value": ""} request.session = {"acr_value": ""}
# Ensure that the CLIENT instance used in login_callback is the mock # Ensure that the CLIENT instance used in login_callback is the mock
# patch requires_step_up_auth to return False # patch _requires_step_up_auth to return False
with patch("djangooidc.views.requires_step_up_auth", return_value=False), patch( with patch("djangooidc.views._requires_step_up_auth", return_value=False), patch(
"djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock() "djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock()
) as mock_create_authn_request: ) as mock_create_authn_request:
# TEST
# test the login callback
login_callback(request) login_callback(request)
# ASSERTIONS
# create_authn_request only gets called when requires_step_up_auth is True # create_authn_request only gets called when _requires_step_up_auth is True
# and it changes this acr_value in request.session # and it changes this acr_value in request.session
# Assert that acr_value is NOT updated by testing that it is still an empty string # Assert that acr_value is NOT updated by testing that it is still an empty string
self.assertEqual(request.session["acr_value"], "") self.assertEqual(request.session["acr_value"], "")
# Assert create_authn_request was not called # Assert create_authn_request was not called
@ -152,31 +289,36 @@ class ViewsTest(TestCase):
@patch("djangooidc.views.authenticate") @patch("djangooidc.views.authenticate")
def test_login_callback_raises(self, mock_auth, mock_client): def test_login_callback_raises(self, mock_auth, mock_client):
# mock """Test that login callback raises a 401 when user is unauthorized"""
with less_console_noise():
# MOCK
# mock that callback returns user_info; this is the expected behavior
mock_client.callback.side_effect = self.user_info mock_client.callback.side_effect = self.user_info
mock_auth.return_value = None mock_auth.return_value = None
# test # 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):
response = self.client.get(reverse("openid_login_callback")) response = self.client.get(reverse("openid_login_callback"))
# assert # ASSERTIONS
self.assertEqual(response.status_code, 401) self.assertEqual(response.status_code, 401)
self.assertTemplateUsed(response, "401.html") self.assertTemplateUsed(response, "401.html")
self.assertIn("Unauthorized", response.content.decode("utf-8")) self.assertIn("Unauthorized", response.content.decode("utf-8"))
def test_logout_redirect_url(self, mock_client): def test_logout_redirect_url(self, mock_client):
# setup """Test that logout redirects to the configured post_logout_redirect_uris."""
with less_console_noise():
# SETUP
session = self.client.session session = self.client.session
session["state"] = "TEST" # nosec B105 session["state"] = "TEST" # nosec B105
session.save() session.save()
# mock # MOCK
mock_client.callback.side_effect = self.user_info mock_client.callback.side_effect = self.user_info
mock_client.registration_response = {"post_logout_redirect_uris": ["http://example.com/back"]} mock_client.registration_response = {"post_logout_redirect_uris": ["http://example.com/back"]}
mock_client.provider_info = {"end_session_endpoint": "http://example.com/log_me_out"} mock_client.provider_info = {"end_session_endpoint": "http://example.com/log_me_out"}
mock_client.client_id = "TEST" mock_client.client_id = "TEST"
# test # TEST
with less_console_noise(): with less_console_noise():
response = self.client.get(reverse("logout")) response = self.client.get(reverse("logout"))
# assert # ASSERTIONS
expected = ( expected = (
"http://example.com/log_me_out?client_id=TEST&state" "http://example.com/log_me_out?client_id=TEST&state"
"=TEST&post_logout_redirect_uri=http%3A%2F%2Fexample.com%2Fback" "=TEST&post_logout_redirect_uri=http%3A%2F%2Fexample.com%2Fback"
@ -185,21 +327,46 @@ class ViewsTest(TestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
def test_logout_redirect_url_with_no_session_state(self, mock_client):
"""Test that logout redirects to the configured post_logout_redirect_uris."""
with less_console_noise():
# MOCK
mock_client.callback.side_effect = self.user_info
mock_client.registration_response = {"post_logout_redirect_uris": ["http://example.com/back"]}
mock_client.provider_info = {"end_session_endpoint": "http://example.com/log_me_out"}
mock_client.client_id = "TEST"
# TEST
with less_console_noise():
response = self.client.get(reverse("logout"))
# ASSERTIONS
# Assert redirect code and url are accurate
expected = (
"http://example.com/log_me_out?client_id=TEST"
"&post_logout_redirect_uri=http%3A%2F%2Fexample.com%2Fback"
)
actual = response.url
self.assertEqual(response.status_code, 302)
self.assertEqual(actual, expected)
@patch("djangooidc.views.auth_logout") @patch("djangooidc.views.auth_logout")
def test_logout_always_logs_out(self, mock_logout, _): def test_logout_always_logs_out(self, mock_logout, _):
# Without additional mocking, logout will always fail. """Without additional mocking, logout will always fail.
# Here we test that auth_logout is called regardless Here we test that auth_logout is called regardless"""
# TEST
with less_console_noise(): with less_console_noise():
self.client.get(reverse("logout")) self.client.get(reverse("logout"))
# ASSERTIONS
self.assertTrue(mock_logout.called) self.assertTrue(mock_logout.called)
def test_logout_callback_redirects(self, _): def test_logout_callback_redirects(self, _):
# setup """Test that the logout_callback redirects properly"""
with less_console_noise():
# SETUP
session = self.client.session session = self.client.session
session["next"] = reverse("logout") session["next"] = reverse("logout")
session.save() session.save()
# test # TEST
response = self.client.get(reverse("openid_logout_callback")) response = self.client.get(reverse("openid_logout_callback"))
# assert # ASSERTIONS
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("logout")) self.assertEqual(response.url, reverse("logout"))

View file

@ -15,15 +15,34 @@ from registrar.models import User
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
try: CLIENT = None
def _initialize_client():
"""Initialize the OIDC client. Exceptions are allowed to raise
and will need to be caught."""
global CLIENT
# Initialize provider using pyOICD # Initialize provider using pyOICD
OP = getattr(settings, "OIDC_ACTIVE_PROVIDER") OP = getattr(settings, "OIDC_ACTIVE_PROVIDER")
CLIENT = Client(OP) CLIENT = Client(OP)
logger.debug("client initialized %s" % CLIENT) logger.debug("Client initialized: %s" % CLIENT)
def _client_is_none():
"""Return if the CLIENT is currently None."""
global CLIENT
return CLIENT is None
# Initialize CLIENT
try:
_initialize_client()
except Exception as err: except Exception as err:
CLIENT = None # type: ignore # In the event of an exception, log the error and allow the app load to continue
logger.warning(err) # without the OIDC Client. Subsequent login attempts will attempt to initialize
logger.warning("Unable to configure OpenID Connect provider. Users cannot log in.") # again if Client is None
logger.error(err)
logger.error("Unable to configure OpenID Connect provider. Users cannot log in.")
def error_page(request, error): def error_page(request, error):
@ -55,13 +74,15 @@ def error_page(request, error):
def openid(request): def openid(request):
"""Redirect the user to an authentication provider (OP).""" """Redirect the user to an authentication provider (OP)."""
global CLIENT
# If the session reset because of a server restart, attempt to login again
request.session["acr_value"] = CLIENT.get_default_acr_value()
request.session["next"] = request.GET.get("next", "/")
try: try:
# If the CLIENT is none, attempt to reinitialize before handling the request
if _client_is_none():
logger.debug("OIDC client is None, attempting to initialize")
_initialize_client()
request.session["acr_value"] = CLIENT.get_default_acr_value()
request.session["next"] = request.GET.get("next", "/")
# Create the authentication request
return CLIENT.create_authn_request(request.session) return CLIENT.create_authn_request(request.session)
except Exception as err: except Exception as err:
return error_page(request, err) return error_page(request, err)
@ -69,12 +90,17 @@ def openid(request):
def login_callback(request): def login_callback(request):
"""Analyze the token returned by the authentication provider (OP).""" """Analyze the token returned by the authentication provider (OP)."""
global CLIENT
try: try:
# If the CLIENT is none, attempt to reinitialize before handling the request
if _client_is_none():
logger.debug("OIDC client is None, attempting to initialize")
_initialize_client()
query = parse_qs(request.GET.urlencode()) query = parse_qs(request.GET.urlencode())
userinfo = CLIENT.callback(query, request.session) userinfo = CLIENT.callback(query, request.session)
# test for need for identity verification and if it is satisfied # test for need for identity verification and if it is satisfied
# if not satisfied, redirect user to login with stepped up acr_value # if not satisfied, redirect user to login with stepped up acr_value
if requires_step_up_auth(userinfo): if _requires_step_up_auth(userinfo):
# add acr_value to request.session # add acr_value to request.session
request.session["acr_value"] = CLIENT.get_step_up_acr_value() request.session["acr_value"] = CLIENT.get_step_up_acr_value()
return CLIENT.create_authn_request(request.session) return CLIENT.create_authn_request(request.session)
@ -87,13 +113,16 @@ def login_callback(request):
else: else:
raise o_e.BannedUser() raise o_e.BannedUser()
except o_e.NoStateDefined as nsd_err: except o_e.NoStateDefined as nsd_err:
# In the event that a user is in the middle of a login when the app is restarted,
# their session state will no longer be available, so redirect the user to the
# beginning of login process without raising an error to the user.
logger.warning(f"No State Defined: {nsd_err}") logger.warning(f"No State Defined: {nsd_err}")
return redirect(request.session.get("next", "/")) return redirect(request.session.get("next", "/"))
except Exception as err: except Exception as err:
return error_page(request, err) return error_page(request, err)
def requires_step_up_auth(userinfo): def _requires_step_up_auth(userinfo):
"""if User.needs_identity_verification and step_up_acr_value not in """if User.needs_identity_verification and step_up_acr_value not in
ial returned from callback, return True""" ial returned from callback, return True"""
step_up_acr_value = CLIENT.get_step_up_acr_value() step_up_acr_value = CLIENT.get_step_up_acr_value()
@ -116,8 +145,12 @@ def logout(request, next_page=None):
user = request.user user = request.user
request_args = { request_args = {
"client_id": CLIENT.client_id, "client_id": CLIENT.client_id,
"state": request.session["state"],
} }
# if state is not in request session, still redirect to the identity
# provider's logout url, but don't include the state in the url; this
# will successfully log out of the identity provider
if "state" in request.session:
request_args["state"] = request.session["state"]
if ( if (
"post_logout_redirect_uris" in CLIENT.registration_response.keys() "post_logout_redirect_uris" in CLIENT.registration_response.keys()
and len(CLIENT.registration_response["post_logout_redirect_uris"]) > 0 and len(CLIENT.registration_response["post_logout_redirect_uris"]) > 0

View file

@ -0,0 +1,51 @@
import os
import logging
from contextlib import contextmanager
def get_handlers():
"""Obtain pointers to all StreamHandlers."""
handlers = {}
rootlogger = logging.getLogger()
for h in rootlogger.handlers:
if isinstance(h, logging.StreamHandler):
handlers[h.name] = h
for logger in logging.Logger.manager.loggerDict.values():
if not isinstance(logger, logging.PlaceHolder):
for h in logger.handlers:
if isinstance(h, logging.StreamHandler):
handlers[h.name] = h
return handlers
@contextmanager
def less_console_noise():
"""
Context manager to use in tests to silence console logging.
This is helpful on tests which trigger console messages
(such as errors) which are normal and expected.
It can easily be removed to debug a failing test.
"""
restore = {}
handlers = get_handlers()
devnull = open(os.devnull, "w")
# redirect all the streams
for handler in handlers.values():
prior = handler.setStream(devnull)
restore[handler.name] = prior
try:
# run the test
yield
finally:
# restore the streams
for handler in handlers.values():
handler.setStream(restore[handler.name])
# close the file we opened
devnull.close()

View file

@ -9,7 +9,7 @@ from epplibwrapper.socket import Socket
from epplibwrapper.utility.pool import EPPConnectionPool from epplibwrapper.utility.pool import EPPConnectionPool
from registrar.models.domain import registry from registrar.models.domain import registry
from contextlib import ExitStack from contextlib import ExitStack
from .common import less_console_noise
import logging import logging
try: try:
@ -135,6 +135,7 @@ class TestConnectionPool(TestCase):
stack.enter_context(patch.object(EPPConnectionPool, "kill_all_connections", do_nothing)) stack.enter_context(patch.object(EPPConnectionPool, "kill_all_connections", do_nothing))
stack.enter_context(patch.object(SocketTransport, "send", self.fake_send)) stack.enter_context(patch.object(SocketTransport, "send", self.fake_send))
stack.enter_context(patch.object(SocketTransport, "receive", fake_receive)) stack.enter_context(patch.object(SocketTransport, "receive", fake_receive))
with less_console_noise():
# Restart the connection pool # Restart the connection pool
registry.start_connection_pool() registry.start_connection_pool()
# Pool should be running, and be the right size # Pool should be running, and be the right size
@ -152,6 +153,8 @@ class TestConnectionPool(TestCase):
# The number of open pools should match the number of requested ones. # The number of open pools should match the number of requested ones.
# If it is 0, then they failed to open # If it is 0, then they failed to open
self.assertEqual(len(registry._pool.conn), self.pool_options["size"]) self.assertEqual(len(registry._pool.conn), self.pool_options["size"])
# Kill the connection pool
registry.kill_pool()
@patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success) @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success)
def test_pool_restarts_on_send(self): def test_pool_restarts_on_send(self):
@ -198,16 +201,22 @@ class TestConnectionPool(TestCase):
xml = (location).read_bytes() xml = (location).read_bytes()
return xml return xml
def do_nothing(command):
pass
# Mock what happens inside the "with" # Mock what happens inside the "with"
with ExitStack() as stack: with ExitStack() as stack:
stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket)) stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket))
stack.enter_context(patch.object(Socket, "connect", self.fake_client)) stack.enter_context(patch.object(Socket, "connect", self.fake_client))
stack.enter_context(patch.object(EPPConnectionPool, "kill_all_connections", do_nothing))
stack.enter_context(patch.object(SocketTransport, "send", self.fake_send)) stack.enter_context(patch.object(SocketTransport, "send", self.fake_send))
stack.enter_context(patch.object(SocketTransport, "receive", fake_receive)) stack.enter_context(patch.object(SocketTransport, "receive", fake_receive))
with less_console_noise():
# Start the connection pool
registry.start_connection_pool()
# Kill the connection pool # Kill the connection pool
registry.kill_pool() registry.kill_pool()
self.assertEqual(registry.pool_status.connection_success, False)
self.assertEqual(registry.pool_status.pool_running, False) self.assertEqual(registry.pool_status.pool_running, False)
# An exception should be raised as end user will be informed # An exception should be raised as end user will be informed
@ -227,6 +236,8 @@ class TestConnectionPool(TestCase):
# The number of open pools should match the number of requested ones. # The number of open pools should match the number of requested ones.
# If it is 0, then they failed to open # If it is 0, then they failed to open
self.assertEqual(len(registry._pool.conn), self.pool_options["size"]) self.assertEqual(len(registry._pool.conn), self.pool_options["size"])
# Kill the connection pool
registry.kill_pool()
@patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success) @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success)
def test_raises_connection_error(self): def test_raises_connection_error(self):
@ -236,6 +247,9 @@ class TestConnectionPool(TestCase):
with ExitStack() as stack: with ExitStack() as stack:
stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket)) stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket))
stack.enter_context(patch.object(Socket, "connect", self.fake_client)) stack.enter_context(patch.object(Socket, "connect", self.fake_client))
with less_console_noise():
# Start the connection pool
registry.start_connection_pool()
# Pool should be running # Pool should be running
self.assertEqual(registry.pool_status.connection_success, True) self.assertEqual(registry.pool_status.connection_success, True)

View file

@ -85,6 +85,21 @@ class EPPConnectionPool(ConnectionPool):
logger.error(message, exc_info=True) logger.error(message, exc_info=True)
raise PoolError(code=PoolErrorCodes.KEEP_ALIVE_FAILED) from err raise PoolError(code=PoolErrorCodes.KEEP_ALIVE_FAILED) from err
def _keepalive_periodic(self):
"""Overriding _keepalive_periodic from geventconnpool so that PoolErrors
are properly handled, as opposed to printing to stdout"""
delay = float(self.keepalive) / self.size
while 1:
try:
with self.get() as c:
self._keepalive(c)
except PoolError as err:
logger.error(err.message, exc_info=True)
except self.exc_classes:
# Nothing to do, the pool will generate a new connection later
pass
gevent.sleep(delay)
def _create_socket(self, client, login) -> Socket: def _create_socket(self, client, login) -> Socket:
"""Creates and returns a socket instance""" """Creates and returns a socket instance"""
socket = Socket(client, login) socket = Socket(client, login)

View file

@ -678,6 +678,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
"type_of_work", "type_of_work",
"more_organization_information", "more_organization_information",
"domain", "domain",
"domain_application",
"submitter", "submitter",
"no_other_contacts_rationale", "no_other_contacts_rationale",
"anything_else", "anything_else",
@ -739,7 +740,6 @@ class DomainApplicationAdminForm(forms.ModelForm):
class DomainApplicationAdmin(ListHeaderAdmin): class DomainApplicationAdmin(ListHeaderAdmin):
"""Custom domain applications admin class.""" """Custom domain applications admin class."""
class InvestigatorFilter(admin.SimpleListFilter): class InvestigatorFilter(admin.SimpleListFilter):
@ -846,6 +846,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
"creator", "creator",
"about_your_organization", "about_your_organization",
"requested_domain", "requested_domain",
"approved_domain",
"alternative_domains", "alternative_domains",
"purpose", "purpose",
"submitter", "submitter",
@ -883,14 +884,11 @@ class DomainApplicationAdmin(ListHeaderAdmin):
if ( if (
obj obj
and original_obj.status == models.DomainApplication.ApplicationStatus.APPROVED and original_obj.status == models.DomainApplication.ApplicationStatus.APPROVED
and ( and obj.status != models.DomainApplication.ApplicationStatus.APPROVED
obj.status == models.DomainApplication.ApplicationStatus.REJECTED
or obj.status == models.DomainApplication.ApplicationStatus.INELIGIBLE
)
and not obj.domain_is_not_active() and not obj.domain_is_not_active()
): ):
# If an admin tried to set an approved application to # If an admin tried to set an approved application to
# rejected or ineligible and the related domain is already # another status and the related domain is already
# active, shortcut the action and throw a friendly # active, shortcut the action and throw a friendly
# error message. This action would still not go through # error message. This action would still not go through
# shortcut or not as the rules are duplicated on the model, # shortcut or not as the rules are duplicated on the model,

View file

@ -2,7 +2,6 @@ from django.apps import AppConfig
class RegistrarConfig(AppConfig): class RegistrarConfig(AppConfig):
"""Configure signal handling for our registrar Django application.""" """Configure signal handling for our registrar Django application."""
name = "registrar" name = "registrar"

View file

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

View file

@ -129,3 +129,28 @@ abbr[title] {
.flex-end { .flex-end {
align-items: flex-end; align-items: flex-end;
} }
// Only apply this custom wrapping to desktop
@include at-media(desktop) {
.usa-tooltip__body {
width: 350px;
white-space: normal;
text-align: center;
}
}
@include at-media(tablet) {
.usa-tooltip__body {
width: 250px !important;
white-space: normal !important;
text-align: center !important;
}
}
@include at-media(mobile) {
.usa-tooltip__body {
width: 250px !important;
white-space: normal !important;
text-align: center !important;
}
}

View file

@ -53,8 +53,8 @@ a.usa-button--unstyled.disabled-link:focus {
} }
.usa-button--unstyled.disabled-button, .usa-button--unstyled.disabled-button,
.usa-button--unstyled.disabled-link:hover, .usa-button--unstyled.disabled-button:hover,
.usa-button--unstyled.disabled-link:focus { .usa-button--unstyled.disabled-button:focus {
cursor: not-allowed !important; cursor: not-allowed !important;
outline: none !important; outline: none !important;
text-decoration: none !important; text-decoration: none !important;

View file

@ -26,6 +26,16 @@
padding-bottom: units(2px); padding-bottom: units(2px);
} }
td .no-click-outline-and-cursor-help {
outline: none;
cursor: help;
use {
// USWDS has weird interactions with SVGs regarding tooltips,
// and other components. In this event, we need to disable pointer interactions.
pointer-events: none;
}
}
// Ticket #1510 // Ticket #1510
// @include at-media('desktop') { // @include at-media('desktop') {
// th:first-child { // th:first-child {

View file

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

View file

@ -16,6 +16,7 @@ $ docker-compose exec app python manage.py shell
``` ```
""" """
import environs import environs
from base64 import b64decode from base64 import b64decode
from cfenv import AppEnv # type: ignore from cfenv import AppEnv # type: ignore

View file

@ -74,7 +74,7 @@ urlpatterns = [
views.ApplicationWithdrawn.as_view(), views.ApplicationWithdrawn.as_view(),
name="application-withdrawn", name="application-withdrawn",
), ),
path("health/", views.health), path("health", views.health, name="health"),
path("openid/", include("djangooidc.urls")), path("openid/", include("djangooidc.urls")),
path("request/", include((application_urls, APPLICATION_NAMESPACE))), path("request/", include((application_urls, APPLICATION_NAMESPACE))),
path("api/v1/available/", available, name="available"), path("api/v1/available/", available, name="available"),

View file

@ -104,7 +104,7 @@ class DomainApplicationFixture:
# Random choice of agency for selects, used as placeholders for testing. # Random choice of agency for selects, used as placeholders for testing.
else random.choice(DomainApplication.AGENCIES) # nosec else random.choice(DomainApplication.AGENCIES) # nosec
) )
da.submission_date = fake.date()
da.federal_type = ( da.federal_type = (
app["federal_type"] app["federal_type"]
if "federal_type" in app if "federal_type" in app
@ -201,7 +201,6 @@ class DomainApplicationFixture:
class DomainFixture(DomainApplicationFixture): class DomainFixture(DomainApplicationFixture):
"""Create one domain and permissions on it for each user.""" """Create one domain and permissions on it for each user."""
@classmethod @classmethod

View file

@ -1,4 +1,5 @@
"""Loads files from /tmp into our sandboxes""" """Loads files from /tmp into our sandboxes"""
import glob import glob
import logging import logging

View file

@ -1,4 +1,5 @@
"""Generates current-full.csv and current-federal.csv then uploads them to the desired URL.""" """Generates current-full.csv and current-federal.csv then uploads them to the desired URL."""
import logging import logging
import os import os

View file

@ -1,4 +1,5 @@
"""Generates current-full.csv and current-federal.csv then uploads them to the desired URL.""" """Generates current-full.csv and current-federal.csv then uploads them to the desired URL."""
import logging import logging
import os import os

View file

@ -1,4 +1,5 @@
"""Loops through each valid DomainInformation object and updates its agency value""" """Loops through each valid DomainInformation object and updates its agency value"""
import argparse import argparse
import csv import csv
import logging import logging

View file

@ -5,6 +5,7 @@ Regarding our dataclasses:
Not intended to be used as models but rather as an alternative to storing as a dictionary. Not intended to be used as models but rather as an alternative to storing as a dictionary.
By keeping it as a dataclass instead of a dictionary, we can maintain data consistency. By keeping it as a dataclass instead of a dictionary, we can maintain data consistency.
""" # noqa """ # noqa
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import date from datetime import date
from enum import Enum from enum import Enum

View file

@ -1,4 +1,5 @@
"""""" """"""
import csv import csv
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime

View file

@ -0,0 +1,45 @@
# Generated by Django 4.2.7 on 2024-02-14 21:45
from django.db import migrations, models
import phonenumber_field.modelfields
class Migration(migrations.Migration):
dependencies = [
("registrar", "0068_domainapplication_notes_domaininformation_notes"),
]
operations = [
migrations.AlterField(
model_name="contact",
name="email",
field=models.EmailField(blank=True, db_index=True, max_length=254, null=True),
),
migrations.AlterField(
model_name="contact",
name="first_name",
field=models.TextField(blank=True, db_index=True, null=True, verbose_name="first name / given name"),
),
migrations.AlterField(
model_name="contact",
name="last_name",
field=models.TextField(blank=True, db_index=True, null=True, verbose_name="last name / family name"),
),
migrations.AlterField(
model_name="contact",
name="middle_name",
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name="contact",
name="phone",
field=phonenumber_field.modelfields.PhoneNumberField(
blank=True, db_index=True, max_length=128, null=True, region=None
),
),
migrations.AlterField(
model_name="contact",
name="title",
field=models.TextField(blank=True, null=True, verbose_name="title or role in your organization"),
),
]

View file

@ -6,7 +6,6 @@ from .utility.time_stamped_model import TimeStampedModel
class Contact(TimeStampedModel): class Contact(TimeStampedModel):
"""Contact information follows a similar pattern for each contact.""" """Contact information follows a similar pattern for each contact."""
user = models.OneToOneField( user = models.OneToOneField(
@ -19,38 +18,32 @@ class Contact(TimeStampedModel):
first_name = models.TextField( first_name = models.TextField(
null=True, null=True,
blank=True, blank=True,
help_text="First name",
verbose_name="first name / given name", verbose_name="first name / given name",
db_index=True, db_index=True,
) )
middle_name = models.TextField( middle_name = models.TextField(
null=True, null=True,
blank=True, blank=True,
help_text="Middle name (optional)",
) )
last_name = models.TextField( last_name = models.TextField(
null=True, null=True,
blank=True, blank=True,
help_text="Last name",
verbose_name="last name / family name", verbose_name="last name / family name",
db_index=True, db_index=True,
) )
title = models.TextField( title = models.TextField(
null=True, null=True,
blank=True, blank=True,
help_text="Title",
verbose_name="title or role in your organization", verbose_name="title or role in your organization",
) )
email = models.EmailField( email = models.EmailField(
null=True, null=True,
blank=True, blank=True,
help_text="Email",
db_index=True, db_index=True,
) )
phone = PhoneNumberField( phone = PhoneNumberField(
null=True, null=True,
blank=True, blank=True,
help_text="Phone",
db_index=True, db_index=True,
) )

View file

@ -12,6 +12,7 @@ from django.utils import timezone
from typing import Any from typing import Any
from registrar.models.host import Host from registrar.models.host import Host
from registrar.models.host_ip import HostIP from registrar.models.host_ip import HostIP
from registrar.utility.enums import DefaultEmail
from registrar.utility.errors import ( from registrar.utility.errors import (
ActionNotAllowed, ActionNotAllowed,
@ -139,6 +140,24 @@ class Domain(TimeStampedModel, DomainHelper):
# previously existed but has been deleted from the registry # previously existed but has been deleted from the registry
DELETED = "deleted", "Deleted" DELETED = "deleted", "Deleted"
@classmethod
def get_help_text(cls, state) -> str:
"""Returns a help message for a desired state. If none is found, an empty string is returned"""
help_texts = {
# For now, unknown has the same message as DNS_NEEDED
cls.UNKNOWN: ("Before this domain can be used, " "youll need to add name server addresses."),
cls.DNS_NEEDED: ("Before this domain can be used, " "youll need to add name server addresses."),
cls.READY: "This domain has name servers and is ready for use.",
cls.ON_HOLD: (
"This domain is administratively paused, "
"so it cant be edited and wont resolve in DNS. "
"Contact help@get.gov for details."
),
cls.DELETED: ("This domain has been removed and " "is no longer registered to your organization."),
}
return help_texts.get(state, "")
class Cache(property): class Cache(property):
""" """
Python descriptor to turn class methods into properties. Python descriptor to turn class methods into properties.
@ -1399,6 +1418,21 @@ class Domain(TimeStampedModel, DomainHelper):
logger.info("Changing to DNS_NEEDED state") logger.info("Changing to DNS_NEEDED state")
logger.info("able to transition to DNS_NEEDED state") logger.info("able to transition to DNS_NEEDED state")
def get_state_help_text(self) -> str:
"""Returns a str containing additional information about a given state.
Returns custom content for when the domain itself is expired."""
if self.is_expired() and self.state != self.State.UNKNOWN:
# Given expired is not a physical state, but it is displayed as such,
# We need custom logic to determine this message.
help_text = (
"This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov."
)
else:
help_text = Domain.State.get_help_text(self.state)
return help_text
def _disclose_fields(self, contact: PublicContact): def _disclose_fields(self, contact: PublicContact):
"""creates a disclose object that can be added to a contact Create using """creates a disclose object that can be added to a contact Create using
.disclose= <this function> on the command before sending. .disclose= <this function> on the command before sending.
@ -1406,7 +1440,9 @@ class Domain(TimeStampedModel, DomainHelper):
is_security = contact.contact_type == contact.ContactTypeChoices.SECURITY is_security = contact.contact_type == contact.ContactTypeChoices.SECURITY
DF = epp.DiscloseField DF = epp.DiscloseField
fields = {DF.EMAIL} fields = {DF.EMAIL}
disclose = is_security and contact.email != PublicContact.get_default_security().email
hidden_security_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value]
disclose = is_security and contact.email not in hidden_security_emails
# Delete after testing on other devices # Delete after testing on other devices
logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose) logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose)
# Will only disclose DF.EMAIL if its not the default # Will only disclose DF.EMAIL if its not the default

View file

@ -17,7 +17,6 @@ logger = logging.getLogger(__name__)
class DomainApplication(TimeStampedModel): class DomainApplication(TimeStampedModel):
"""A registrant's application for a new domain.""" """A registrant's application for a new domain."""
# Constants for choice fields # Constants for choice fields
@ -97,7 +96,6 @@ class DomainApplication(TimeStampedModel):
ARMED_FORCES_AP = "AP", "Armed Forces Pacific (AP)" ARMED_FORCES_AP = "AP", "Armed Forces Pacific (AP)"
class OrganizationChoices(models.TextChoices): class OrganizationChoices(models.TextChoices):
""" """
Primary organization choices: Primary organization choices:
For use in django admin For use in django admin
@ -114,7 +112,6 @@ class DomainApplication(TimeStampedModel):
SCHOOL_DISTRICT = "school_district", "School district" SCHOOL_DISTRICT = "school_district", "School district"
class OrganizationChoicesVerbose(models.TextChoices): class OrganizationChoicesVerbose(models.TextChoices):
""" """
Secondary organization choices Secondary organization choices
For use in the application form and on the templates For use in the application form and on the templates
@ -578,6 +575,19 @@ class DomainApplication(TimeStampedModel):
return not self.approved_domain.is_active() return not self.approved_domain.is_active()
return True return True
def delete_and_clean_up_domain(self, called_from):
try:
domain_state = self.approved_domain.state
# Only reject if it exists on EPP
if domain_state != Domain.State.UNKNOWN:
self.approved_domain.deletedInEpp()
self.approved_domain.save()
self.approved_domain.delete()
self.approved_domain = None
except Exception as err:
logger.error(err)
logger.error(f"Can't query an approved domain while attempting {called_from}")
def _send_status_update_email(self, new_status, email_template, email_template_subject, send_email=True): def _send_status_update_email(self, new_status, email_template, email_template_subject, send_email=True):
"""Send a status update email to the submitter. """Send a status update email to the submitter.
@ -641,6 +651,10 @@ class DomainApplication(TimeStampedModel):
self.submission_date = timezone.now().date() self.submission_date = timezone.now().date()
self.save() self.save()
# Limit email notifications to transitions from Started and Withdrawn
limited_statuses = [self.ApplicationStatus.STARTED, self.ApplicationStatus.WITHDRAWN]
if self.status in limited_statuses:
self._send_status_update_email( self._send_status_update_email(
"submission confirmation", "submission confirmation",
"emails/submission_confirmation.txt", "emails/submission_confirmation.txt",
@ -657,11 +671,19 @@ class DomainApplication(TimeStampedModel):
ApplicationStatus.INELIGIBLE, ApplicationStatus.INELIGIBLE,
], ],
target=ApplicationStatus.IN_REVIEW, target=ApplicationStatus.IN_REVIEW,
conditions=[domain_is_not_active],
) )
def in_review(self): def in_review(self):
"""Investigate an application that has been submitted. """Investigate an application that has been submitted.
This action is logged.""" This action is logged.
As side effects this will delete the domain and domain_information
(will cascade) when they exist."""
if self.status == self.ApplicationStatus.APPROVED:
self.delete_and_clean_up_domain("in_review")
literal = DomainApplication.ApplicationStatus.IN_REVIEW literal = DomainApplication.ApplicationStatus.IN_REVIEW
# Check if the tuple exists, then grab its value # Check if the tuple exists, then grab its value
in_review = literal if literal is not None else "In Review" in_review = literal if literal is not None else "In Review"
@ -676,11 +698,19 @@ class DomainApplication(TimeStampedModel):
ApplicationStatus.INELIGIBLE, ApplicationStatus.INELIGIBLE,
], ],
target=ApplicationStatus.ACTION_NEEDED, target=ApplicationStatus.ACTION_NEEDED,
conditions=[domain_is_not_active],
) )
def action_needed(self): def action_needed(self):
"""Send back an application that is under investigation or rejected. """Send back an application that is under investigation or rejected.
This action is logged.""" This action is logged.
As side effects this will delete the domain and domain_information
(will cascade) when they exist."""
if self.status == self.ApplicationStatus.APPROVED:
self.delete_and_clean_up_domain("reject_with_prejudice")
literal = DomainApplication.ApplicationStatus.ACTION_NEEDED literal = DomainApplication.ApplicationStatus.ACTION_NEEDED
# Check if the tuple is setup correctly, then grab its value # Check if the tuple is setup correctly, then grab its value
action_needed = literal if literal is not None else "Action Needed" action_needed = literal if literal is not None else "Action Needed"
@ -735,6 +765,7 @@ class DomainApplication(TimeStampedModel):
) )
def withdraw(self): def withdraw(self):
"""Withdraw an application that has been submitted.""" """Withdraw an application that has been submitted."""
self._send_status_update_email( self._send_status_update_email(
"withdraw", "withdraw",
"emails/domain_request_withdrawn.txt", "emails/domain_request_withdrawn.txt",
@ -752,18 +783,9 @@ class DomainApplication(TimeStampedModel):
As side effects this will delete the domain and domain_information As side effects this will delete the domain and domain_information
(will cascade), and send an email notification.""" (will cascade), and send an email notification."""
if self.status == self.ApplicationStatus.APPROVED: if self.status == self.ApplicationStatus.APPROVED:
try: self.delete_and_clean_up_domain("reject")
domain_state = self.approved_domain.state
# Only reject if it exists on EPP
if domain_state != Domain.State.UNKNOWN:
self.approved_domain.deletedInEpp()
self.approved_domain.save()
self.approved_domain.delete()
self.approved_domain = None
except Exception as err:
logger.error(err)
logger.error("Can't query an approved domain while attempting a DA reject()")
self._send_status_update_email( self._send_status_update_email(
"action needed", "action needed",
@ -792,17 +814,7 @@ class DomainApplication(TimeStampedModel):
and domain_information (will cascade) when they exist.""" and domain_information (will cascade) when they exist."""
if self.status == self.ApplicationStatus.APPROVED: if self.status == self.ApplicationStatus.APPROVED:
try: self.delete_and_clean_up_domain("reject_with_prejudice")
domain_state = self.approved_domain.state
# Only reject if it exists on EPP
if domain_state != Domain.State.UNKNOWN:
self.approved_domain.deletedInEpp()
self.approved_domain.save()
self.approved_domain.delete()
self.approved_domain = None
except Exception as err:
logger.error(err)
logger.error("Can't query an approved domain while attempting a DA reject_with_prejudice()")
self.creator.restrict_user() self.creator.restrict_user()

View file

@ -14,7 +14,6 @@ logger = logging.getLogger(__name__)
class DomainInformation(TimeStampedModel): class DomainInformation(TimeStampedModel):
"""A registrant's domain information for that domain, exported from """A registrant's domain information for that domain, exported from
DomainApplication. We use these field from DomainApplication with few exceptions DomainApplication. We use these field from DomainApplication with few exceptions
which are 'removed' via pop at the bottom of this file. Most of design for domain which are 'removed' via pop at the bottom of this file. Most of design for domain
@ -256,6 +255,14 @@ class DomainInformation(TimeStampedModel):
else: else:
da_many_to_many_dict[field] = getattr(domain_application, field).all() da_many_to_many_dict[field] = getattr(domain_application, field).all()
# This will not happen in normal code flow, but having some redundancy doesn't hurt.
# da_dict should not have "id" under any circumstances.
# If it does have it, then this indicates that common_fields is overzealous in the data
# that it is returning. Try looking in DomainHelper.get_common_fields.
if "id" in da_dict:
logger.warning("create_from_da() -> Found attribute 'id' when trying to create")
da_dict.pop("id", None)
# Create a placeholder DomainInformation object # Create a placeholder DomainInformation object
domain_info = DomainInformation(**da_dict) domain_info = DomainInformation(**da_dict)

View file

@ -4,6 +4,8 @@ from string import ascii_uppercase, ascii_lowercase, digits
from django.db import models from django.db import models
from registrar.utility.enums import DefaultEmail
from .utility.time_stamped_model import TimeStampedModel from .utility.time_stamped_model import TimeStampedModel
@ -87,7 +89,7 @@ class PublicContact(TimeStampedModel):
sp="VA", sp="VA",
pc="20598-0645", pc="20598-0645",
cc="US", cc="US",
email="dotgov@cisa.dhs.gov", email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value,
voice="+1.8882820870", voice="+1.8882820870",
pw="thisisnotapassword", pw="thisisnotapassword",
) )
@ -104,7 +106,7 @@ class PublicContact(TimeStampedModel):
sp="VA", sp="VA",
pc="22201", pc="22201",
cc="US", cc="US",
email="dotgov@cisa.dhs.gov", email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value,
voice="+1.8882820870", voice="+1.8882820870",
pw="thisisnotapassword", pw="thisisnotapassword",
) )
@ -121,7 +123,7 @@ class PublicContact(TimeStampedModel):
sp="VA", sp="VA",
pc="22201", pc="22201",
cc="US", cc="US",
email="dotgov@cisa.dhs.gov", email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value,
voice="+1.8882820870", voice="+1.8882820870",
pw="thisisnotapassword", pw="thisisnotapassword",
) )
@ -138,7 +140,7 @@ class PublicContact(TimeStampedModel):
sp="VA", sp="VA",
pc="22201", pc="22201",
cc="US", cc="US",
email="dotgov@cisa.dhs.gov", email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value,
voice="+1.8882820870", voice="+1.8882820870",
pw="thisisnotapassword", pw="thisisnotapassword",
) )

View file

@ -4,11 +4,9 @@ from .utility.time_stamped_model import TimeStampedModel
class UserDomainRole(TimeStampedModel): class UserDomainRole(TimeStampedModel):
"""This is a linking table that connects a user with a role on a domain.""" """This is a linking table that connects a user with a role on a domain."""
class Roles(models.TextChoices): class Roles(models.TextChoices):
"""The possible roles are listed here. """The possible roles are listed here.
Implementation of the named roles for allowing particular operations happens Implementation of the named roles for allowing particular operations happens

View file

@ -180,8 +180,8 @@ class DomainHelper:
""" """
# Get a list of the existing fields on model_1 and model_2 # Get a list of the existing fields on model_1 and model_2
model_1_fields = set(field.name for field in model_1._meta.get_fields() if field != "id") model_1_fields = set(field.name for field in model_1._meta.get_fields() if field.name != "id")
model_2_fields = set(field.name for field in model_2._meta.get_fields() if field != "id") model_2_fields = set(field.name for field in model_2._meta.get_fields() if field.name != "id")
# Get the fields that exist on both DomainApplication and DomainInformation # Get the fields that exist on both DomainApplication and DomainInformation
common_fields = model_1_fields & model_2_fields common_fields = model_1_fields & model_2_fields

View file

@ -4,7 +4,6 @@ from .utility.time_stamped_model import TimeStampedModel
class VerifiedByStaff(TimeStampedModel): class VerifiedByStaff(TimeStampedModel):
"""emails that get added to this table will bypass ial2 on login.""" """emails that get added to this table will bypass ial2 on login."""
email = models.EmailField( email = models.EmailField(

View file

@ -4,7 +4,6 @@ from .utility.time_stamped_model import TimeStampedModel
class Website(TimeStampedModel): class Website(TimeStampedModel):
"""Keep domain names in their own table so that applications can refer to """Keep domain names in their own table so that applications can refer to
many of them.""" many of them."""

View file

@ -6,7 +6,6 @@ better caching responses.
class NoCacheMiddleware: class NoCacheMiddleware:
"""Middleware to add a single header to every response.""" """Middleware to add a single header to every response."""
def __init__(self, get_response): def __init__(self, get_response):

View file

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

View file

@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static form_helpers url_helpers %} {% load static form_helpers url_helpers %}
{% block title %}Request a .gov domain | {{form_titles|get_item:steps.current}} | {% endblock %} {% block title %}{{form_titles|get_item:steps.current}} | Request a .gov | {% endblock %}
{% block content %} {% block content %}
<div class="grid-container"> <div class="grid-container">
<div class="grid-row grid-gap"> <div class="grid-row grid-gap">

View file

@ -8,11 +8,13 @@
<li class="usa-sidenav__item sidenav__step--locked"> <li class="usa-sidenav__item sidenav__step--locked">
<span> <span>
{% if not this_step == steps.current %} {% if not this_step == steps.current %}
{% if this_step != "review" %}
<svg class="usa-icon text-green" aria-hidden="true" focsuable="false" role="img" width="24" height="24"> <svg class="usa-icon text-green" aria-hidden="true" focsuable="false" role="img" width="24" height="24">
<title id="checked-step__{{forloop.counter}}">Checked mark</title> <title id="checked-step__{{forloop.counter}}">Checked mark</title>
<use xlink:href="{%static 'img/sprite.svg'%}#check_circle"></use> <use xlink:href="{%static 'img/sprite.svg'%}#check_circle"></use>
</svg> </svg>
{% endif %} {% endif %}
{% endif %}
<a href="{% namespaced_url 'application' this_step %}" <a href="{% namespaced_url 'application' this_step %}"
{% if this_step == steps.current %} {% if this_step == steps.current %}
class="usa-current" class="usa-current"

View file

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

View file

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

View file

@ -6,7 +6,7 @@
<div class="margin-top-4 tablet:grid-col-10"> <div class="margin-top-4 tablet:grid-col-10">
<div <div
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2{% if not domain.is_expired %}{% if domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %} dotgov-status-box--action-need{% endif %}{% endif %}" class="usa-summary-box dotgov-status-box padding-bottom-0 margin-top-3 padding-left-2{% if not domain.is_expired %}{% if domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %} dotgov-status-box--action-need{% endif %}{% endif %}"
role="region" role="region"
aria-labelledby="summary-box-key-information" aria-labelledby="summary-box-key-information"
> >
@ -17,6 +17,7 @@
<span class="text-bold text-primary-darker"> <span class="text-bold text-primary-darker">
Status: Status:
</span> </span>
<span class="text-primary-darker">
{# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #} {# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #}
{% if domain.is_expired and domain.state != domain.State.UNKNOWN %} {% if domain.is_expired and domain.state != domain.State.UNKNOWN %}
Expired Expired
@ -25,6 +26,12 @@
{% else %} {% else %}
{{ domain.state|title }} {{ domain.state|title }}
{% endif %} {% endif %}
</span>
{% if domain.get_state_help_text %}
<div class="padding-top-1 text-primary-darker">
{{ domain.get_state_help_text }}
</div>
{% endif %}
</p> </p>
</div> </div>
</div> </div>
@ -56,7 +63,7 @@
{% include "includes/summary_item.html" with title='Your contact information' value=request.user.contact contact='true' edit_link=url editable=domain.is_editable %} {% include "includes/summary_item.html" with title='Your contact information' value=request.user.contact contact='true' edit_link=url editable=domain.is_editable %}
{% url 'domain-security-email' pk=domain.id as url %} {% url 'domain-security-email' pk=domain.id as url %}
{% if security_email is not None and security_email != default_security_email%} {% if security_email is not None and security_email not in hidden_security_emails%}
{% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url editable=domain.is_editable %} {% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url editable=domain.is_editable %}
{% else %} {% else %}
{% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url editable=domain.is_editable %} {% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url editable=domain.is_editable %}

View file

@ -15,6 +15,7 @@
{% endblock %} {% endblock %}
<h1>Manage your domains</h2> <h1>Manage your domains</h2>
<p class="margin-top-4"> <p class="margin-top-4">
<a href="{% url 'application:' %}" class="usa-button" <a href="{% url 'application:' %}" class="usa-button"
> >
@ -56,6 +57,16 @@
{% else %} {% else %}
{{ domain.state|capfirst }} {{ domain.state|capfirst }}
{% endif %} {% endif %}
<svg
class="usa-icon usa-tooltip text-middle margin-bottom-05 text-accent-cool no-click-outline-and-cursor-help"
data-position="top"
title="{{domain.get_state_help_text}}"
focusable="true"
aria-label="Status Information"
role="tooltip"
>
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#info_outline"></use>
</svg>
</td> </td>
<td> <td>
<a href="{% url "domain" pk=domain.pk %}"> <a href="{% url "domain" pk=domain.pk %}">

View file

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

View file

@ -1,4 +1,5 @@
"""Custom field helpers for our inputs.""" """Custom field helpers for our inputs."""
import re import re
from django import template from django import template

View file

@ -12,6 +12,7 @@ from typing import List, Dict
from django.contrib.sessions.middleware import SessionMiddleware from django.contrib.sessions.middleware import SessionMiddleware
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model, login from django.contrib.auth import get_user_model, login
from django.utils.timezone import make_aware
from registrar.models import ( from registrar.models import (
Contact, Contact,
@ -643,7 +644,7 @@ class MockEppLib(TestCase):
self, self,
id, id,
email, email,
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
pw="thisisnotapassword", pw="thisisnotapassword",
): ):
fake = info.InfoContactResultData( fake = info.InfoContactResultData(
@ -681,7 +682,7 @@ class MockEppLib(TestCase):
mockDataInfoDomain = fakedEppObject( mockDataInfoDomain = fakedEppObject(
"fakePw", "fakePw",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)], contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
hosts=["fake.host.com"], hosts=["fake.host.com"],
statuses=[ statuses=[
@ -692,7 +693,7 @@ class MockEppLib(TestCase):
) )
mockDataExtensionDomain = fakedEppObject( mockDataExtensionDomain = fakedEppObject(
"fakePw", "fakePw",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)], contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
hosts=["fake.host.com"], hosts=["fake.host.com"],
statuses=[ statuses=[
@ -706,7 +707,7 @@ class MockEppLib(TestCase):
) )
InfoDomainWithContacts = fakedEppObject( InfoDomainWithContacts = fakedEppObject(
"fakepw", "fakepw",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
contacts=[ contacts=[
common.DomainContact( common.DomainContact(
contact="securityContact", contact="securityContact",
@ -731,7 +732,7 @@ class MockEppLib(TestCase):
InfoDomainWithDefaultSecurityContact = fakedEppObject( InfoDomainWithDefaultSecurityContact = fakedEppObject(
"fakepw", "fakepw",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
contacts=[ contacts=[
common.DomainContact( common.DomainContact(
contact="defaultSec", contact="defaultSec",
@ -750,7 +751,7 @@ class MockEppLib(TestCase):
) )
InfoDomainWithVerisignSecurityContact = fakedEppObject( InfoDomainWithVerisignSecurityContact = fakedEppObject(
"fakepw", "fakepw",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
contacts=[ contacts=[
common.DomainContact( common.DomainContact(
contact="defaultVeri", contact="defaultVeri",
@ -766,7 +767,7 @@ class MockEppLib(TestCase):
InfoDomainWithDefaultTechnicalContact = fakedEppObject( InfoDomainWithDefaultTechnicalContact = fakedEppObject(
"fakepw", "fakepw",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
contacts=[ contacts=[
common.DomainContact( common.DomainContact(
contact="defaultTech", contact="defaultTech",
@ -791,14 +792,14 @@ class MockEppLib(TestCase):
infoDomainNoContact = fakedEppObject( infoDomainNoContact = fakedEppObject(
"security", "security",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
contacts=[], contacts=[],
hosts=["fake.host.com"], hosts=["fake.host.com"],
) )
infoDomainThreeHosts = fakedEppObject( infoDomainThreeHosts = fakedEppObject(
"my-nameserver.gov", "my-nameserver.gov",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
contacts=[], contacts=[],
hosts=[ hosts=[
"ns1.my-nameserver-1.com", "ns1.my-nameserver-1.com",
@ -809,25 +810,25 @@ class MockEppLib(TestCase):
infoDomainNoHost = fakedEppObject( infoDomainNoHost = fakedEppObject(
"my-nameserver.gov", "my-nameserver.gov",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
contacts=[], contacts=[],
hosts=[], hosts=[],
) )
infoDomainTwoHosts = fakedEppObject( infoDomainTwoHosts = fakedEppObject(
"my-nameserver.gov", "my-nameserver.gov",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
contacts=[], contacts=[],
hosts=["ns1.my-nameserver-1.com", "ns1.my-nameserver-2.com"], hosts=["ns1.my-nameserver-1.com", "ns1.my-nameserver-2.com"],
) )
mockDataInfoHosts = fakedEppObject( mockDataInfoHosts = fakedEppObject(
"lastPw", "lastPw",
cr_date=datetime.datetime(2023, 8, 25, 19, 45, 35), cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)),
addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")], addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")],
) )
mockDataHostChange = fakedEppObject("lastPw", cr_date=datetime.datetime(2023, 8, 25, 19, 45, 35)) mockDataHostChange = fakedEppObject("lastPw", cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)))
addDsData1 = { addDsData1 = {
"keyTag": 1234, "keyTag": 1234,
"alg": 3, "alg": 3,
@ -859,7 +860,7 @@ class MockEppLib(TestCase):
infoDomainHasIP = fakedEppObject( infoDomainHasIP = fakedEppObject(
"nameserverwithip.gov", "nameserverwithip.gov",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
contacts=[ contacts=[
common.DomainContact( common.DomainContact(
contact="securityContact", contact="securityContact",
@ -884,7 +885,7 @@ class MockEppLib(TestCase):
justNameserver = fakedEppObject( justNameserver = fakedEppObject(
"justnameserver.com", "justnameserver.com",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
contacts=[ contacts=[
common.DomainContact( common.DomainContact(
contact="securityContact", contact="securityContact",
@ -907,7 +908,7 @@ class MockEppLib(TestCase):
infoDomainCheckHostIPCombo = fakedEppObject( infoDomainCheckHostIPCombo = fakedEppObject(
"nameserversubdomain.gov", "nameserversubdomain.gov",
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
contacts=[], contacts=[],
hosts=[ hosts=[
"ns1.nameserversubdomain.gov", "ns1.nameserversubdomain.gov",

View file

@ -234,11 +234,11 @@ class TestDomainAdmin(MockEppLib, WebTest):
""" """
Make sure the short name is displaying in admin on the list page Make sure the short name is displaying in admin on the list page
""" """
with less_console_noise():
self.client.force_login(self.superuser) self.client.force_login(self.superuser)
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
mock_client = MockSESClient() mock_client = MockSESClient()
with boto3_mocking.clients.handler_for("sesv2", mock_client): with boto3_mocking.clients.handler_for("sesv2", mock_client):
with less_console_noise():
application.approve() application.approve()
response = self.client.get("/admin/registrar/domain/") response = self.client.get("/admin/registrar/domain/")
@ -295,12 +295,12 @@ class TestDomainAdmin(MockEppLib, WebTest):
Then a user-friendly success message is returned for displaying on the web Then a user-friendly success message is returned for displaying on the web
And `state` is et to `DELETED` And `state` is et to `DELETED`
""" """
with less_console_noise():
domain = create_ready_domain() domain = create_ready_domain()
# Put in client hold # Put in client hold
domain.place_client_hold() domain.place_client_hold()
p = "userpass" p = "userpass"
self.client.login(username="staffuser", password=p) self.client.login(username="staffuser", password=p)
# Ensure everything is displaying correctly # Ensure everything is displaying correctly
response = self.client.get( response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.pk), "/admin/registrar/domain/{}/change/".format(domain.pk),
@ -309,7 +309,6 @@ class TestDomainAdmin(MockEppLib, WebTest):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name) self.assertContains(response, domain.name)
self.assertContains(response, "Remove from registry") self.assertContains(response, "Remove from registry")
# Test the info dialog # Test the info dialog
request = self.factory.post( request = self.factory.post(
"/admin/registrar/domain/{}/change/".format(domain.pk), "/admin/registrar/domain/{}/change/".format(domain.pk),
@ -317,7 +316,6 @@ class TestDomainAdmin(MockEppLib, WebTest):
follow=True, follow=True,
) )
request.user = self.client request.user = self.client
with patch("django.contrib.messages.add_message") as mock_add_message: with patch("django.contrib.messages.add_message") as mock_add_message:
self.admin.do_delete_domain(request, domain) self.admin.do_delete_domain(request, domain)
mock_add_message.assert_called_once_with( mock_add_message.assert_called_once_with(
@ -327,7 +325,6 @@ class TestDomainAdmin(MockEppLib, WebTest):
extra_tags="", extra_tags="",
fail_silently=False, fail_silently=False,
) )
self.assertEqual(domain.state, Domain.State.DELETED) self.assertEqual(domain.state, Domain.State.DELETED)
def test_deletion_ready_fsm_failure(self): def test_deletion_ready_fsm_failure(self):
@ -337,10 +334,10 @@ class TestDomainAdmin(MockEppLib, WebTest):
Then a user-friendly error message is returned for displaying on the web Then a user-friendly error message is returned for displaying on the web
And `state` is not set to `DELETED` And `state` is not set to `DELETED`
""" """
with less_console_noise():
domain = create_ready_domain() domain = create_ready_domain()
p = "userpass" p = "userpass"
self.client.login(username="staffuser", password=p) self.client.login(username="staffuser", password=p)
# Ensure everything is displaying correctly # Ensure everything is displaying correctly
response = self.client.get( response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.pk), "/admin/registrar/domain/{}/change/".format(domain.pk),
@ -349,7 +346,6 @@ class TestDomainAdmin(MockEppLib, WebTest):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name) self.assertContains(response, domain.name)
self.assertContains(response, "Remove from registry") self.assertContains(response, "Remove from registry")
# Test the error # Test the error
request = self.factory.post( request = self.factory.post(
"/admin/registrar/domain/{}/change/".format(domain.pk), "/admin/registrar/domain/{}/change/".format(domain.pk),
@ -357,7 +353,6 @@ class TestDomainAdmin(MockEppLib, WebTest):
follow=True, follow=True,
) )
request.user = self.client request.user = self.client
with patch("django.contrib.messages.add_message") as mock_add_message: with patch("django.contrib.messages.add_message") as mock_add_message:
self.admin.do_delete_domain(request, domain) self.admin.do_delete_domain(request, domain)
mock_add_message.assert_called_once_with( mock_add_message.assert_called_once_with(
@ -380,12 +375,12 @@ class TestDomainAdmin(MockEppLib, WebTest):
Then `commands.DeleteDomain` is sent to the registry Then `commands.DeleteDomain` is sent to the registry
And Domain returns normally without an error dialog And Domain returns normally without an error dialog
""" """
with less_console_noise():
domain = create_ready_domain() domain = create_ready_domain()
# Put in client hold # Put in client hold
domain.place_client_hold() domain.place_client_hold()
p = "userpass" p = "userpass"
self.client.login(username="staffuser", password=p) self.client.login(username="staffuser", password=p)
# Ensure everything is displaying correctly # Ensure everything is displaying correctly
response = self.client.get( response = self.client.get(
"/admin/registrar/domain/{}/change/".format(domain.pk), "/admin/registrar/domain/{}/change/".format(domain.pk),
@ -394,7 +389,6 @@ class TestDomainAdmin(MockEppLib, WebTest):
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name) self.assertContains(response, domain.name)
self.assertContains(response, "Remove from registry") self.assertContains(response, "Remove from registry")
# Test the info dialog # Test the info dialog
request = self.factory.post( request = self.factory.post(
"/admin/registrar/domain/{}/change/".format(domain.pk), "/admin/registrar/domain/{}/change/".format(domain.pk),
@ -402,7 +396,6 @@ class TestDomainAdmin(MockEppLib, WebTest):
follow=True, follow=True,
) )
request.user = self.client request.user = self.client
# Delete it once # Delete it once
with patch("django.contrib.messages.add_message") as mock_add_message: with patch("django.contrib.messages.add_message") as mock_add_message:
self.admin.do_delete_domain(request, domain) self.admin.do_delete_domain(request, domain)
@ -415,7 +408,6 @@ class TestDomainAdmin(MockEppLib, WebTest):
) )
self.assertEqual(domain.state, Domain.State.DELETED) self.assertEqual(domain.state, Domain.State.DELETED)
# Try to delete it again # Try to delete it again
# Test the info dialog # Test the info dialog
request = self.factory.post( request = self.factory.post(
@ -424,7 +416,6 @@ class TestDomainAdmin(MockEppLib, WebTest):
follow=True, follow=True,
) )
request.user = self.client request.user = self.client
with patch("django.contrib.messages.add_message") as mock_add_message: with patch("django.contrib.messages.add_message") as mock_add_message:
self.admin.do_delete_domain(request, domain) self.admin.do_delete_domain(request, domain)
mock_add_message.assert_called_once_with( mock_add_message.assert_called_once_with(
@ -434,7 +425,6 @@ class TestDomainAdmin(MockEppLib, WebTest):
extra_tags="", extra_tags="",
fail_silently=False, fail_silently=False,
) )
self.assertEqual(domain.state, Domain.State.DELETED) self.assertEqual(domain.state, Domain.State.DELETED)
@skip("Waiting on epp lib to implement") @skip("Waiting on epp lib to implement")
@ -491,6 +481,7 @@ class TestDomainApplicationAdminForm(TestCase):
) )
@boto3_mocking.patching
class TestDomainApplicationAdmin(MockEppLib): class TestDomainApplicationAdmin(MockEppLib):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
@ -596,83 +587,166 @@ class TestDomainApplicationAdmin(MockEppLib):
# Now let's make sure the long description does not exist # Now let's make sure the long description does not exist
self.assertNotContains(response, "Federal: an agency of the U.S. government") self.assertNotContains(response, "Federal: an agency of the U.S. government")
@boto3_mocking.patching def transition_state_and_send_email(self, application, status):
def test_save_model_sends_submitted_email(self): """Helper method for the email test cases."""
# make sure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
with boto3_mocking.clients.handler_for("sesv2", self.mock_client): with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise(): with less_console_noise():
# Create a mock request
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Modify the application's property
application.status = status
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
def assert_email_is_accurate(self, expected_string, email_index, email_address):
"""Helper method for the email test cases.
email_index is the index of the email in mock_client."""
# Access the arguments passed to send_email
call_args = self.mock_client.EMAILS_SENT
kwargs = call_args[email_index]["kwargs"]
# Retrieve the email details from the arguments
from_email = kwargs.get("FromEmailAddress")
to_email = kwargs["Destination"]["ToAddresses"][0]
email_content = kwargs["Content"]
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
# Assert or perform other checks on the email details
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
self.assertEqual(to_email, email_address)
self.assertIn(expected_string, email_body)
def test_save_model_sends_submitted_email(self):
"""When transitioning to submitted from started or withdrawn on a domain request,
an email is sent out.
When transitioning to submitted from dns needed or in review on a domain request,
no email is sent out."""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create a sample application # Create a sample application
application = completed_application() application = completed_application()
# Create a mock request # Test Submitted Status from started
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL)
# Modify the application's property
application.status = DomainApplication.ApplicationStatus.SUBMITTED
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = self.mock_client.EMAILS_SENT
kwargs = call_args[0]["kwargs"]
# Retrieve the email details from the arguments
from_email = kwargs.get("FromEmailAddress")
to_email = kwargs["Destination"]["ToAddresses"][0]
email_content = kwargs["Content"]
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
# Assert or perform other checks on the email details
expected_string = "We received your .gov domain request."
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
self.assertEqual(to_email, EMAIL)
self.assertIn(expected_string, email_body)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
@boto3_mocking.patching # Test Withdrawn Status
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN)
self.assert_email_is_accurate(
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
# Test Submitted Status Again (from withdrawn)
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Move it to IN_REVIEW
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Test Submitted Status Again from in IN_REVIEW, no new email should be sent
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Move it to IN_REVIEW
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.IN_REVIEW)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Move it to ACTION_NEEDED
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.ACTION_NEEDED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
# Test Submitted Status Again from in ACTION_NEEDED, no new email should be sent
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
def test_save_model_sends_approved_email(self): def test_save_model_sends_approved_email(self):
# make sure there is no user with this email """When transitioning to approved on a domain request,
an email is sent out every time."""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov" EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete() User.objects.filter(email=EMAIL).delete()
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
# Create a sample application # Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Create a mock request # Test Submitted Status
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 0, EMAIL)
# Modify the application's property
application.status = DomainApplication.ApplicationStatus.APPROVED
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = self.mock_client.EMAILS_SENT
kwargs = call_args[0]["kwargs"]
# Retrieve the email details from the arguments
from_email = kwargs.get("FromEmailAddress")
to_email = kwargs["Destination"]["ToAddresses"][0]
email_content = kwargs["Content"]
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
# Assert or perform other checks on the email details
expected_string = "Congratulations! Your .gov domain request has been approved."
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
self.assertEqual(to_email, EMAIL)
self.assertIn(expected_string, email_body)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1) self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
@boto3_mocking.patching # Test Withdrawn Status
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED)
self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
# Test Submitted Status Again (No new email should be sent)
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
def test_save_model_sends_rejected_email(self):
"""When transitioning to rejected on a domain request,
an email is sent out every time."""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Test Submitted Status
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED)
self.assert_email_is_accurate("Your .gov domain request has been rejected.", 0, EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Test Withdrawn Status
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.APPROVED)
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
# Test Submitted Status Again (No new email should be sent)
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.REJECTED)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
def test_save_model_sends_withdrawn_email(self):
"""When transitioning to withdrawn on a domain request,
an email is sent out every time."""
# Ensure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Test Submitted Status
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN)
self.assert_email_is_accurate(
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 0, EMAIL
)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
# Test Withdrawn Status
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.SUBMITTED)
self.assert_email_is_accurate("We received your .gov domain request.", 1, EMAIL)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
# Test Submitted Status Again (No new email should be sent)
self.transition_state_and_send_email(application, DomainApplication.ApplicationStatus.WITHDRAWN)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
def test_save_model_sets_approved_domain(self): def test_save_model_sets_approved_domain(self):
# make sure there is no user with this email # make sure there is no user with this email
EMAIL = "mayor@igorville.gov" EMAIL = "mayor@igorville.gov"
@ -695,45 +769,6 @@ class TestDomainApplicationAdmin(MockEppLib):
# Test that approved domain exists and equals requested domain # Test that approved domain exists and equals requested domain
self.assertEqual(application.requested_domain.name, application.approved_domain.name) self.assertEqual(application.requested_domain.name, application.approved_domain.name)
@boto3_mocking.patching
def test_save_model_sends_rejected_email(self):
# make sure there is no user with this email
EMAIL = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete()
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
# Create a sample application
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
# Create a mock request
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
# Modify the application's property
application.status = DomainApplication.ApplicationStatus.REJECTED
# Use the model admin's save_model method
self.admin.save_model(request, application, form=None, change=True)
# Access the arguments passed to send_email
call_args = self.mock_client.EMAILS_SENT
kwargs = call_args[0]["kwargs"]
# Retrieve the email details from the arguments
from_email = kwargs.get("FromEmailAddress")
to_email = kwargs["Destination"]["ToAddresses"][0]
email_content = kwargs["Content"]
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
# Assert or perform other checks on the email details
expected_string = "Your .gov domain request has been rejected."
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
self.assertEqual(to_email, EMAIL)
self.assertIn(expected_string, email_body)
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
@boto3_mocking.patching
def test_save_model_sets_restricted_status_on_user(self): def test_save_model_sets_restricted_status_on_user(self):
# make sure there is no user with this email # make sure there is no user with this email
EMAIL = "mayor@igorville.gov" EMAIL = "mayor@igorville.gov"
@ -817,6 +852,7 @@ class TestDomainApplicationAdmin(MockEppLib):
"creator", "creator",
"about_your_organization", "about_your_organization",
"requested_domain", "requested_domain",
"approved_domain",
"alternative_domains", "alternative_domains",
"purpose", "purpose",
"submitter", "submitter",
@ -883,41 +919,13 @@ class TestDomainApplicationAdmin(MockEppLib):
"Cannot edit an application with a restricted creator.", "Cannot edit an application with a restricted creator.",
) )
@boto3_mocking.patching def trigger_saving_approved_to_another_state(self, domain_is_active, another_state):
def test_error_when_saving_approved_to_rejected_and_domain_is_active(self): """Helper method that triggers domain request state changes from approved to another state,
# Create an instance of the model with an associated domain that can be either active (READY) or not.
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
application.approved_domain = domain
application.save()
# Create a request object with a superuser Used to test errors when saving a change with an active domain, also used to test side effects
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) when saving a change goes through."""
request.user = self.superuser
# Define a custom implementation for is_active
def custom_is_active(self):
return True # Override to return True
# Use ExitStack to combine patch contexts
with ExitStack() as stack:
# Patch Domain.is_active and django.contrib.messages.error simultaneously
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
stack.enter_context(patch.object(messages, "error"))
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
# Simulate saving the model
application.status = DomainApplication.ApplicationStatus.REJECTED
self.admin.save_model(request, application, None, True)
# Assert that the error message was called with the correct argument
messages.error.assert_called_once_with(
request,
"This action is not permitted. The domain " + "is already active.",
)
def test_side_effects_when_saving_approved_to_rejected(self):
# Create an instance of the model # Create an instance of the model
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name) domain = Domain.objects.create(name=application.requested_domain.name)
@ -931,19 +939,24 @@ class TestDomainApplicationAdmin(MockEppLib):
# Define a custom implementation for is_active # Define a custom implementation for is_active
def custom_is_active(self): def custom_is_active(self):
return False # Override to return False return domain_is_active # Override to return True
# Use ExitStack to combine patch contexts # Use ExitStack to combine patch contexts
with ExitStack() as stack: with ExitStack() as stack:
# Patch Domain.is_active and django.contrib.messages.error simultaneously # Patch Domain.is_active and django.contrib.messages.error simultaneously
stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
stack.enter_context(patch.object(messages, "error")) stack.enter_context(patch.object(messages, "error"))
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise(): application.status = another_state
# Simulate saving the model
application.status = DomainApplication.ApplicationStatus.REJECTED
self.admin.save_model(request, application, None, True) self.admin.save_model(request, application, None, True)
# Assert that the error message was called with the correct argument
if domain_is_active:
messages.error.assert_called_once_with(
request,
"This action is not permitted. The domain " + "is already active.",
)
else:
# Assert that the error message was never called # Assert that the error message was never called
messages.error.assert_not_called() messages.error.assert_not_called()
@ -957,75 +970,29 @@ class TestDomainApplicationAdmin(MockEppLib):
with self.assertRaises(DomainInformation.DoesNotExist): with self.assertRaises(DomainInformation.DoesNotExist):
domain_information.refresh_from_db() domain_information.refresh_from_db()
def test_error_when_saving_approved_to_in_review_and_domain_is_active(self):
self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.IN_REVIEW)
def test_error_when_saving_approved_to_action_needed_and_domain_is_active(self):
self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.ACTION_NEEDED)
def test_error_when_saving_approved_to_rejected_and_domain_is_active(self):
self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.REJECTED)
def test_error_when_saving_approved_to_ineligible_and_domain_is_active(self): def test_error_when_saving_approved_to_ineligible_and_domain_is_active(self):
# Create an instance of the model self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.INELIGIBLE)
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
application.approved_domain = domain
application.save()
# Create a request object with a superuser def test_side_effects_when_saving_approved_to_in_review(self):
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.IN_REVIEW)
request.user = self.superuser
# Define a custom implementation for is_active def test_side_effects_when_saving_approved_to_action_needed(self):
def custom_is_active(self): self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.ACTION_NEEDED)
return True # Override to return True
# Use ExitStack to combine patch contexts def test_side_effects_when_saving_approved_to_rejected(self):
with ExitStack() as stack: self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.REJECTED)
# Patch Domain.is_active and django.contrib.messages.error simultaneously
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
stack.enter_context(patch.object(messages, "error"))
# Simulate saving the model
application.status = DomainApplication.ApplicationStatus.INELIGIBLE
self.admin.save_model(request, application, None, True)
# Assert that the error message was called with the correct argument
messages.error.assert_called_once_with(
request,
"This action is not permitted. The domain " + "is already active.",
)
def test_side_effects_when_saving_approved_to_ineligible(self): def test_side_effects_when_saving_approved_to_ineligible(self):
# Create an instance of the model self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.INELIGIBLE)
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
domain = Domain.objects.create(name=application.requested_domain.name)
domain_information = DomainInformation.objects.create(creator=self.superuser, domain=domain)
application.approved_domain = domain
application.save()
# Create a request object with a superuser
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
request.user = self.superuser
# Define a custom implementation for is_active
def custom_is_active(self):
return False # Override to return False
# Use ExitStack to combine patch contexts
with ExitStack() as stack:
# Patch Domain.is_active and django.contrib.messages.error simultaneously
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
stack.enter_context(patch.object(messages, "error"))
# Simulate saving the model
application.status = DomainApplication.ApplicationStatus.INELIGIBLE
self.admin.save_model(request, application, None, True)
# Assert that the error message was never called
messages.error.assert_not_called()
self.assertEqual(application.approved_domain, None)
# Assert that Domain got Deleted
with self.assertRaises(Domain.DoesNotExist):
domain.refresh_from_db()
# Assert that DomainInformation got Deleted
with self.assertRaises(DomainInformation.DoesNotExist):
domain_information.refresh_from_db()
def test_has_correct_filters(self): def test_has_correct_filters(self):
""" """
@ -1242,7 +1209,7 @@ class DomainInvitationAdminTest(TestCase):
self.assertContains(response, retrieved_html, count=1) self.assertContains(response, retrieved_html, count=1)
class DomainInformationAdminTest(TestCase): class TestDomainInformationAdmin(TestCase):
def setUp(self): def setUp(self):
"""Setup environment for a mock admin user""" """Setup environment for a mock admin user"""
self.site = AdminSite() self.site = AdminSite()
@ -1250,6 +1217,7 @@ class DomainInformationAdminTest(TestCase):
self.admin = DomainInformationAdmin(model=DomainInformation, admin_site=self.site) self.admin = DomainInformationAdmin(model=DomainInformation, admin_site=self.site)
self.client = Client(HTTP_HOST="localhost:8080") self.client = Client(HTTP_HOST="localhost:8080")
self.superuser = create_superuser() self.superuser = create_superuser()
self.staffuser = create_user()
self.mock_data_generator = AuditedAdminMockData() self.mock_data_generator = AuditedAdminMockData()
self.test_helper = GenericTestHelper( self.test_helper = GenericTestHelper(
@ -1293,6 +1261,27 @@ class DomainInformationAdminTest(TestCase):
Contact.objects.all().delete() Contact.objects.all().delete()
User.objects.all().delete() User.objects.all().delete()
def test_readonly_fields_for_analyst(self):
"""Ensures that analysts have their permissions setup correctly"""
request = self.factory.get("/")
request.user = self.staffuser
readonly_fields = self.admin.get_readonly_fields(request)
expected_fields = [
"creator",
"type_of_work",
"more_organization_information",
"domain",
"domain_application",
"submitter",
"no_other_contacts_rationale",
"anything_else",
"is_policy_acknowledged",
]
self.assertEqual(readonly_fields, expected_fields)
def test_domain_sortable(self): def test_domain_sortable(self):
"""Tests if DomainInformation sorts by domain correctly""" """Tests if DomainInformation sorts by domain correctly"""
p = "adminpass" p = "adminpass"
@ -1457,13 +1446,12 @@ class ListHeaderAdminTest(TestCase):
self.superuser = create_superuser() self.superuser = create_superuser()
def test_changelist_view(self): def test_changelist_view(self):
with less_console_noise():
# Have to get creative to get past linter # Have to get creative to get past linter
p = "adminpass" p = "adminpass"
self.client.login(username="superuser", password=p) self.client.login(username="superuser", password=p)
# Mock a user # Mock a user
user = mock_user() user = mock_user()
# Make the request using the Client class # Make the request using the Client class
# which handles CSRF # which handles CSRF
# Follow=True handles the redirect # Follow=True handles the redirect
@ -1476,7 +1464,6 @@ class ListHeaderAdminTest(TestCase):
}, },
follow=True, follow=True,
) )
# Assert that the filters and search_query are added to the extra_context # Assert that the filters and search_query are added to the extra_context
self.assertIn("filters", response.context) self.assertIn("filters", response.context)
self.assertIn("search_query", response.context) self.assertIn("search_query", response.context)
@ -1496,6 +1483,7 @@ class ListHeaderAdminTest(TestCase):
) )
def test_get_filters(self): def test_get_filters(self):
with less_console_noise():
# Create a mock request object # Create a mock request object
request = self.factory.get("/admin/yourmodel/") request = self.factory.get("/admin/yourmodel/")
# Set the GET parameters for testing # Set the GET parameters for testing
@ -1506,7 +1494,6 @@ class ListHeaderAdminTest(TestCase):
} }
# Call the get_filters method # Call the get_filters method
filters = self.admin.get_filters(request) filters = self.admin.get_filters(request)
# Assert the filters extracted from the request GET # Assert the filters extracted from the request GET
self.assertEqual( self.assertEqual(
filters, filters,
@ -1953,9 +1940,8 @@ class ContactAdminTest(TestCase):
def test_change_view_for_joined_contact_five_or_more(self): def test_change_view_for_joined_contact_five_or_more(self):
"""Create a contact, join it to 5 domain requests. The 6th join will be a user. """Create a contact, join it to 5 domain requests. The 6th join will be a user.
Assert that the warning on the contact form lists 5 joins and a '1 more' ellispsis.""" Assert that the warning on the contact form lists 5 joins and a '1 more' ellispsis."""
with less_console_noise():
self.client.force_login(self.superuser) self.client.force_login(self.superuser)
# Create an instance of the model # Create an instance of the model
# join it to 5 domain requests. The 6th join will be a user. # join it to 5 domain requests. The 6th join will be a user.
contact, _ = Contact.objects.get_or_create(user=self.staffuser) contact, _ = Contact.objects.get_or_create(user=self.staffuser)
@ -1964,13 +1950,10 @@ class ContactAdminTest(TestCase):
application3 = completed_application(submitter=contact, name="city3.gov") application3 = completed_application(submitter=contact, name="city3.gov")
application4 = completed_application(submitter=contact, name="city4.gov") application4 = completed_application(submitter=contact, name="city4.gov")
application5 = completed_application(submitter=contact, name="city5.gov") application5 = completed_application(submitter=contact, name="city5.gov")
with patch("django.contrib.messages.warning") as mock_warning: with patch("django.contrib.messages.warning") as mock_warning:
# Use the test client to simulate the request # Use the test client to simulate the request
response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk])) response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk]))
logger.debug(mock_warning)
logger.info(mock_warning)
# Assert that the error message was called with the correct argument # Assert that the error message was called with the correct argument
# Note: The 6th join will be a user. # Note: The 6th join will be a user.
mock_warning.assert_called_once_with( mock_warning.assert_called_once_with(

View file

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

View file

@ -1,5 +1,6 @@
import copy import copy
import datetime from datetime import date, datetime, time
from django.utils import timezone
from django.test import TestCase from django.test import TestCase
@ -17,7 +18,7 @@ from django.core.management import call_command
from unittest.mock import patch, call from unittest.mock import patch, call
from epplibwrapper import commands, common from epplibwrapper import commands, common
from .common import MockEppLib from .common import MockEppLib, less_console_noise
class TestPopulateFirstReady(TestCase): class TestPopulateFirstReady(TestCase):
@ -33,7 +34,9 @@ class TestPopulateFirstReady(TestCase):
self.unknown_domain, _ = Domain.objects.get_or_create(name="fakeunknown.gov", state=Domain.State.UNKNOWN) self.unknown_domain, _ = Domain.objects.get_or_create(name="fakeunknown.gov", state=Domain.State.UNKNOWN)
# Set a ready_at date for testing purposes # Set a ready_at date for testing purposes
self.ready_at_date = datetime.date(2022, 12, 31) self.ready_at_date = date(2022, 12, 31)
_ready_at_datetime = datetime.combine(self.ready_at_date, time.min)
self.ready_at_date_tz_aware = timezone.make_aware(_ready_at_datetime, timezone=timezone.utc)
def tearDown(self): def tearDown(self):
"""Deletes all DB objects related to migrations""" """Deletes all DB objects related to migrations"""
@ -49,6 +52,7 @@ class TestPopulateFirstReady(TestCase):
The 'call_command' function from Django's management framework is then used to The 'call_command' function from Django's management framework is then used to
execute the populate_first_ready command with the specified arguments. execute the populate_first_ready command with the specified arguments.
""" """
with less_console_noise():
with patch( with patch(
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
return_value=True, return_value=True,
@ -59,19 +63,15 @@ class TestPopulateFirstReady(TestCase):
""" """
Tests that the populate_first_ready works as expected for the state 'ready' Tests that the populate_first_ready works as expected for the state 'ready'
""" """
with less_console_noise():
# Set the created at date # Set the created at date
self.ready_domain.created_at = self.ready_at_date self.ready_domain.created_at = self.ready_at_date_tz_aware
self.ready_domain.save() self.ready_domain.save()
desired_domain = copy.deepcopy(self.ready_domain) desired_domain = copy.deepcopy(self.ready_domain)
desired_domain.first_ready = self.ready_at_date desired_domain.first_ready = self.ready_at_date
# Run the expiration date script # Run the expiration date script
self.run_populate_first_ready() self.run_populate_first_ready()
self.assertEqual(desired_domain, self.ready_domain) self.assertEqual(desired_domain, self.ready_domain)
# Explicitly test the first_ready date # Explicitly test the first_ready date
first_ready = Domain.objects.filter(name="fakeready.gov").get().first_ready first_ready = Domain.objects.filter(name="fakeready.gov").get().first_ready
self.assertEqual(first_ready, self.ready_at_date) self.assertEqual(first_ready, self.ready_at_date)
@ -80,19 +80,15 @@ class TestPopulateFirstReady(TestCase):
""" """
Tests that the populate_first_ready works as expected for the state 'deleted' Tests that the populate_first_ready works as expected for the state 'deleted'
""" """
with less_console_noise():
# Set the created at date # Set the created at date
self.deleted_domain.created_at = self.ready_at_date self.deleted_domain.created_at = self.ready_at_date_tz_aware
self.deleted_domain.save() self.deleted_domain.save()
desired_domain = copy.deepcopy(self.deleted_domain) desired_domain = copy.deepcopy(self.deleted_domain)
desired_domain.first_ready = self.ready_at_date desired_domain.first_ready = self.ready_at_date
# Run the expiration date script # Run the expiration date script
self.run_populate_first_ready() self.run_populate_first_ready()
self.assertEqual(desired_domain, self.deleted_domain) self.assertEqual(desired_domain, self.deleted_domain)
# Explicitly test the first_ready date # Explicitly test the first_ready date
first_ready = Domain.objects.filter(name="fakedeleted.gov").get().first_ready first_ready = Domain.objects.filter(name="fakedeleted.gov").get().first_ready
self.assertEqual(first_ready, self.ready_at_date) self.assertEqual(first_ready, self.ready_at_date)
@ -101,23 +97,18 @@ class TestPopulateFirstReady(TestCase):
""" """
Tests that the populate_first_ready doesn't make changes when a domain's state is 'dns_needed' Tests that the populate_first_ready doesn't make changes when a domain's state is 'dns_needed'
""" """
with less_console_noise():
# Set the created at date # Set the created at date
self.dns_needed_domain.created_at = self.ready_at_date self.dns_needed_domain.created_at = self.ready_at_date_tz_aware
self.dns_needed_domain.save() self.dns_needed_domain.save()
desired_domain = copy.deepcopy(self.dns_needed_domain) desired_domain = copy.deepcopy(self.dns_needed_domain)
desired_domain.first_ready = None desired_domain.first_ready = None
# Run the expiration date script # Run the expiration date script
self.run_populate_first_ready() self.run_populate_first_ready()
current_domain = self.dns_needed_domain current_domain = self.dns_needed_domain
# The object should largely be unaltered (does not test first_ready) # The object should largely be unaltered (does not test first_ready)
self.assertEqual(desired_domain, current_domain) self.assertEqual(desired_domain, current_domain)
first_ready = Domain.objects.filter(name="fakedns.gov").get().first_ready first_ready = Domain.objects.filter(name="fakedns.gov").get().first_ready
# Explicitly test the first_ready date # Explicitly test the first_ready date
self.assertNotEqual(first_ready, self.ready_at_date) self.assertNotEqual(first_ready, self.ready_at_date)
self.assertEqual(first_ready, None) self.assertEqual(first_ready, None)
@ -126,18 +117,15 @@ class TestPopulateFirstReady(TestCase):
""" """
Tests that the populate_first_ready works as expected for the state 'on_hold' Tests that the populate_first_ready works as expected for the state 'on_hold'
""" """
self.hold_domain.created_at = self.ready_at_date with less_console_noise():
self.hold_domain.created_at = self.ready_at_date_tz_aware
self.hold_domain.save() self.hold_domain.save()
desired_domain = copy.deepcopy(self.hold_domain) desired_domain = copy.deepcopy(self.hold_domain)
desired_domain.first_ready = self.ready_at_date desired_domain.first_ready = self.ready_at_date
# Run the update first ready_at script # Run the update first ready_at script
self.run_populate_first_ready() self.run_populate_first_ready()
current_domain = self.hold_domain current_domain = self.hold_domain
self.assertEqual(desired_domain, current_domain) self.assertEqual(desired_domain, current_domain)
# Explicitly test the first_ready date # Explicitly test the first_ready date
first_ready = Domain.objects.filter(name="fakehold.gov").get().first_ready first_ready = Domain.objects.filter(name="fakehold.gov").get().first_ready
self.assertEqual(first_ready, self.ready_at_date) self.assertEqual(first_ready, self.ready_at_date)
@ -146,21 +134,17 @@ class TestPopulateFirstReady(TestCase):
""" """
Tests that the populate_first_ready works as expected for the state 'unknown' Tests that the populate_first_ready works as expected for the state 'unknown'
""" """
with less_console_noise():
# Set the created at date # Set the created at date
self.unknown_domain.created_at = self.ready_at_date self.unknown_domain.created_at = self.ready_at_date_tz_aware
self.unknown_domain.save() self.unknown_domain.save()
desired_domain = copy.deepcopy(self.unknown_domain) desired_domain = copy.deepcopy(self.unknown_domain)
desired_domain.first_ready = None desired_domain.first_ready = None
# Run the expiration date script # Run the expiration date script
self.run_populate_first_ready() self.run_populate_first_ready()
current_domain = self.unknown_domain current_domain = self.unknown_domain
# The object should largely be unaltered (does not test first_ready) # The object should largely be unaltered (does not test first_ready)
self.assertEqual(desired_domain, current_domain) self.assertEqual(desired_domain, current_domain)
# Explicitly test the first_ready date # Explicitly test the first_ready date
first_ready = Domain.objects.filter(name="fakeunknown.gov").get().first_ready first_ready = Domain.objects.filter(name="fakeunknown.gov").get().first_ready
self.assertNotEqual(first_ready, self.ready_at_date) self.assertNotEqual(first_ready, self.ready_at_date)
@ -185,6 +169,7 @@ class TestPatchAgencyInfo(TestCase):
@patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", return_value=True) @patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", return_value=True)
def call_patch_federal_agency_info(self, mock_prompt): def call_patch_federal_agency_info(self, mock_prompt):
"""Calls the patch_federal_agency_info command and mimics a keypress""" """Calls the patch_federal_agency_info command and mimics a keypress"""
with less_console_noise():
call_command("patch_federal_agency_info", "registrar/tests/data/fake_current_full.csv", debug=True) call_command("patch_federal_agency_info", "registrar/tests/data/fake_current_full.csv", debug=True)
def test_patch_agency_info(self): def test_patch_agency_info(self):
@ -194,15 +179,12 @@ class TestPatchAgencyInfo(TestCase):
of a `DomainInformation` object when the corresponding of a `DomainInformation` object when the corresponding
`TransitionDomain` object has a valid `federal_agency`. `TransitionDomain` object has a valid `federal_agency`.
""" """
with less_console_noise():
# Ensure that the federal_agency is None # Ensure that the federal_agency is None
self.assertEqual(self.domain_info.federal_agency, None) self.assertEqual(self.domain_info.federal_agency, None)
self.call_patch_federal_agency_info() self.call_patch_federal_agency_info()
# Reload the domain_info object from the database # Reload the domain_info object from the database
self.domain_info.refresh_from_db() self.domain_info.refresh_from_db()
# Check that the federal_agency field was updated # Check that the federal_agency field was updated
self.assertEqual(self.domain_info.federal_agency, "test agency") self.assertEqual(self.domain_info.federal_agency, "test agency")
@ -213,19 +195,16 @@ class TestPatchAgencyInfo(TestCase):
of a `DomainInformation` object when the corresponding of a `DomainInformation` object when the corresponding
`TransitionDomain` object does not exist. `TransitionDomain` object does not exist.
""" """
with less_console_noise():
# Set federal_agency to None to simulate a skip # Set federal_agency to None to simulate a skip
self.transition_domain.federal_agency = None self.transition_domain.federal_agency = None
self.transition_domain.save() self.transition_domain.save()
with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context: with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context:
self.call_patch_federal_agency_info() self.call_patch_federal_agency_info()
# Check that the correct log message was output # Check that the correct log message was output
self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0]) self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0])
# Reload the domain_info object from the database # Reload the domain_info object from the database
self.domain_info.refresh_from_db() self.domain_info.refresh_from_db()
# Check that the federal_agency field was not updated # Check that the federal_agency field was not updated
self.assertIsNone(self.domain_info.federal_agency) self.assertIsNone(self.domain_info.federal_agency)
@ -235,23 +214,19 @@ class TestPatchAgencyInfo(TestCase):
updates the DomainInformation object, because a record exists in the updates the DomainInformation object, because a record exists in the
provided current-full.csv file. provided current-full.csv file.
""" """
with less_console_noise():
# Set federal_agency to None to simulate a skip # Set federal_agency to None to simulate a skip
self.transition_domain.federal_agency = None self.transition_domain.federal_agency = None
self.transition_domain.save() self.transition_domain.save()
# Change the domain name to something parsable in the .csv # Change the domain name to something parsable in the .csv
self.domain.name = "cdomain1.gov" self.domain.name = "cdomain1.gov"
self.domain.save() self.domain.save()
with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context: with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context:
self.call_patch_federal_agency_info() self.call_patch_federal_agency_info()
# Check that the correct log message was output # Check that the correct log message was output
self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0]) self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0])
# Reload the domain_info object from the database # Reload the domain_info object from the database
self.domain_info.refresh_from_db() self.domain_info.refresh_from_db()
# Check that the federal_agency field was not updated # Check that the federal_agency field was not updated
self.assertEqual(self.domain_info.federal_agency, "World War I Centennial Commission") self.assertEqual(self.domain_info.federal_agency, "World War I Centennial Commission")
@ -261,18 +236,15 @@ class TestPatchAgencyInfo(TestCase):
does not update the `federal_agency` field does not update the `federal_agency` field
of a `DomainInformation` object of a `DomainInformation` object
""" """
with less_console_noise():
self.domain_info.federal_agency = "unchanged" self.domain_info.federal_agency = "unchanged"
self.domain_info.save() self.domain_info.save()
with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="INFO") as context: with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="INFO") as context:
self.call_patch_federal_agency_info() self.call_patch_federal_agency_info()
# Check that the correct log message was output # Check that the correct log message was output
self.assertIn("FINISHED", context.output[1]) self.assertIn("FINISHED", context.output[1])
# Reload the domain_info object from the database # Reload the domain_info object from the database
self.domain_info.refresh_from_db() self.domain_info.refresh_from_db()
# Check that the federal_agency field was not updated # Check that the federal_agency field was not updated
self.assertEqual(self.domain_info.federal_agency, "unchanged") self.assertEqual(self.domain_info.federal_agency, "unchanged")
@ -283,39 +255,37 @@ class TestExtendExpirationDates(MockEppLib):
super().setUp() super().setUp()
# Create a valid domain that is updatable # Create a valid domain that is updatable
Domain.objects.get_or_create( Domain.objects.get_or_create(
name="waterbutpurple.gov", state=Domain.State.READY, expiration_date=datetime.date(2023, 11, 15) name="waterbutpurple.gov", state=Domain.State.READY, expiration_date=date(2023, 11, 15)
) )
TransitionDomain.objects.get_or_create( TransitionDomain.objects.get_or_create(
username="testytester@mail.com", username="testytester@mail.com",
domain_name="waterbutpurple.gov", domain_name="waterbutpurple.gov",
epp_expiration_date=datetime.date(2023, 11, 15), epp_expiration_date=date(2023, 11, 15),
) )
# Create a domain with an invalid expiration date # Create a domain with an invalid expiration date
Domain.objects.get_or_create( Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY, expiration_date=date(2022, 5, 25))
name="fake.gov", state=Domain.State.READY, expiration_date=datetime.date(2022, 5, 25)
)
TransitionDomain.objects.get_or_create( TransitionDomain.objects.get_or_create(
username="themoonisactuallycheese@mail.com", username="themoonisactuallycheese@mail.com",
domain_name="fake.gov", domain_name="fake.gov",
epp_expiration_date=datetime.date(2022, 5, 25), epp_expiration_date=date(2022, 5, 25),
) )
# Create a domain with an invalid state # Create a domain with an invalid state
Domain.objects.get_or_create( Domain.objects.get_or_create(
name="fakeneeded.gov", state=Domain.State.DNS_NEEDED, expiration_date=datetime.date(2023, 11, 15) name="fakeneeded.gov", state=Domain.State.DNS_NEEDED, expiration_date=date(2023, 11, 15)
) )
TransitionDomain.objects.get_or_create( TransitionDomain.objects.get_or_create(
username="fakeneeded@mail.com", username="fakeneeded@mail.com",
domain_name="fakeneeded.gov", domain_name="fakeneeded.gov",
epp_expiration_date=datetime.date(2023, 11, 15), epp_expiration_date=date(2023, 11, 15),
) )
# Create a domain with a date greater than the maximum # Create a domain with a date greater than the maximum
Domain.objects.get_or_create( Domain.objects.get_or_create(
name="fakemaximum.gov", state=Domain.State.READY, expiration_date=datetime.date(2024, 12, 31) name="fakemaximum.gov", state=Domain.State.READY, expiration_date=date(2024, 12, 31)
) )
TransitionDomain.objects.get_or_create( TransitionDomain.objects.get_or_create(
username="fakemaximum@mail.com", username="fakemaximum@mail.com",
domain_name="fakemaximum.gov", domain_name="fakemaximum.gov",
epp_expiration_date=datetime.date(2024, 12, 31), epp_expiration_date=date(2024, 12, 31),
) )
def tearDown(self): def tearDown(self):
@ -338,6 +308,7 @@ class TestExtendExpirationDates(MockEppLib):
The 'call_command' function from Django's management framework is then used to The 'call_command' function from Django's management framework is then used to
execute the extend_expiration_dates command with the specified arguments. execute the extend_expiration_dates command with the specified arguments.
""" """
with less_console_noise():
with patch( with patch(
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
return_value=True, return_value=True,
@ -348,44 +319,41 @@ class TestExtendExpirationDates(MockEppLib):
""" """
Tests that the extend_expiration_dates method extends dates as expected Tests that the extend_expiration_dates method extends dates as expected
""" """
with less_console_noise():
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get() desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
desired_domain.expiration_date = datetime.date(2024, 11, 15) desired_domain.expiration_date = date(2024, 11, 15)
# Run the expiration date script # Run the expiration date script
self.run_extend_expiration_dates() self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get() current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
self.assertEqual(desired_domain, current_domain) self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date # Explicitly test the expiration date
self.assertEqual(current_domain.expiration_date, datetime.date(2024, 11, 15)) self.assertEqual(current_domain.expiration_date, date(2024, 11, 15))
def test_extends_expiration_date_skips_non_current(self): def test_extends_expiration_date_skips_non_current(self):
""" """
Tests that the extend_expiration_dates method correctly skips domains Tests that the extend_expiration_dates method correctly skips domains
with an expiration date less than a certain threshold. with an expiration date less than a certain threshold.
""" """
with less_console_noise():
desired_domain = Domain.objects.filter(name="fake.gov").get() desired_domain = Domain.objects.filter(name="fake.gov").get()
desired_domain.expiration_date = datetime.date(2022, 5, 25) desired_domain.expiration_date = date(2022, 5, 25)
# Run the expiration date script # Run the expiration date script
self.run_extend_expiration_dates() self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="fake.gov").get() current_domain = Domain.objects.filter(name="fake.gov").get()
self.assertEqual(desired_domain, current_domain) self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date. The extend_expiration_dates script # Explicitly test the expiration date. The extend_expiration_dates script
# will skip all dates less than date(2023, 11, 15), meaning that this domain # will skip all dates less than date(2023, 11, 15), meaning that this domain
# should not be affected by the change. # should not be affected by the change.
self.assertEqual(current_domain.expiration_date, datetime.date(2022, 5, 25)) self.assertEqual(current_domain.expiration_date, date(2022, 5, 25))
def test_extends_expiration_date_skips_maximum_date(self): def test_extends_expiration_date_skips_maximum_date(self):
""" """
Tests that the extend_expiration_dates method correctly skips domains Tests that the extend_expiration_dates method correctly skips domains
with an expiration date more than a certain threshold. with an expiration date more than a certain threshold.
""" """
with less_console_noise():
desired_domain = Domain.objects.filter(name="fakemaximum.gov").get() desired_domain = Domain.objects.filter(name="fakemaximum.gov").get()
desired_domain.expiration_date = datetime.date(2024, 12, 31) desired_domain.expiration_date = date(2024, 12, 31)
# Run the expiration date script # Run the expiration date script
self.run_extend_expiration_dates() self.run_extend_expiration_dates()
@ -396,14 +364,15 @@ class TestExtendExpirationDates(MockEppLib):
# Explicitly test the expiration date. The extend_expiration_dates script # Explicitly test the expiration date. The extend_expiration_dates script
# will skip all dates less than date(2023, 11, 15), meaning that this domain # will skip all dates less than date(2023, 11, 15), meaning that this domain
# should not be affected by the change. # should not be affected by the change.
self.assertEqual(current_domain.expiration_date, datetime.date(2024, 12, 31)) self.assertEqual(current_domain.expiration_date, date(2024, 12, 31))
def test_extends_expiration_date_skips_non_ready(self): def test_extends_expiration_date_skips_non_ready(self):
""" """
Tests that the extend_expiration_dates method correctly skips domains not in the state "ready" Tests that the extend_expiration_dates method correctly skips domains not in the state "ready"
""" """
with less_console_noise():
desired_domain = Domain.objects.filter(name="fakeneeded.gov").get() desired_domain = Domain.objects.filter(name="fakeneeded.gov").get()
desired_domain.expiration_date = datetime.date(2023, 11, 15) desired_domain.expiration_date = date(2023, 11, 15)
# Run the expiration date script # Run the expiration date script
self.run_extend_expiration_dates() self.run_extend_expiration_dates()
@ -414,7 +383,7 @@ class TestExtendExpirationDates(MockEppLib):
# Explicitly test the expiration date. The extend_expiration_dates script # Explicitly test the expiration date. The extend_expiration_dates script
# will skip all dates less than date(2023, 11, 15), meaning that this domain # will skip all dates less than date(2023, 11, 15), meaning that this domain
# should not be affected by the change. # should not be affected by the change.
self.assertEqual(current_domain.expiration_date, datetime.date(2023, 11, 15)) self.assertEqual(current_domain.expiration_date, date(2023, 11, 15))
def test_extends_expiration_date_idempotent(self): def test_extends_expiration_date_idempotent(self):
""" """
@ -423,26 +392,21 @@ class TestExtendExpirationDates(MockEppLib):
Verifies that running the method multiple times does not change the expiration date Verifies that running the method multiple times does not change the expiration date
of a domain beyond the initial extension. of a domain beyond the initial extension.
""" """
with less_console_noise():
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get() desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
desired_domain.expiration_date = datetime.date(2024, 11, 15) desired_domain.expiration_date = date(2024, 11, 15)
# Run the expiration date script # Run the expiration date script
self.run_extend_expiration_dates() self.run_extend_expiration_dates()
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get() current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
self.assertEqual(desired_domain, current_domain) self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date # Explicitly test the expiration date
self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15)) self.assertEqual(desired_domain.expiration_date, date(2024, 11, 15))
# Run the expiration date script again # Run the expiration date script again
self.run_extend_expiration_dates() self.run_extend_expiration_dates()
# The old domain shouldn't have changed # The old domain shouldn't have changed
self.assertEqual(desired_domain, current_domain) self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date - should be the same # Explicitly test the expiration date - should be the same
self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15)) self.assertEqual(desired_domain.expiration_date, date(2024, 11, 15))
class TestDiscloseEmails(MockEppLib): class TestDiscloseEmails(MockEppLib):
@ -461,6 +425,7 @@ class TestDiscloseEmails(MockEppLib):
The 'call_command' function from Django's management framework is then used to The 'call_command' function from Django's management framework is then used to
execute the disclose_security_emails command. execute the disclose_security_emails command.
""" """
with less_console_noise():
with patch( with patch(
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
return_value=True, return_value=True,
@ -472,6 +437,7 @@ class TestDiscloseEmails(MockEppLib):
Tests that command disclose_security_emails runs successfully with Tests that command disclose_security_emails runs successfully with
appropriate EPP calll to UpdateContact. appropriate EPP calll to UpdateContact.
""" """
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="testdisclose.gov", state=Domain.State.READY) domain, _ = Domain.objects.get_or_create(name="testdisclose.gov", state=Domain.State.READY)
expectedSecContact = PublicContact.get_default_security() expectedSecContact = PublicContact.get_default_security()
expectedSecContact.domain = domain expectedSecContact.domain = domain

View file

@ -60,23 +60,27 @@ class TestDomainApplication(TestCase):
def assertNotRaises(self, exception_type): def assertNotRaises(self, exception_type):
"""Helper method for testing allowed transitions.""" """Helper method for testing allowed transitions."""
with less_console_noise():
return self.assertRaises(Exception, None, exception_type) return self.assertRaises(Exception, None, exception_type)
def test_empty_create_fails(self): def test_empty_create_fails(self):
"""Can't create a completely empty domain application. """Can't create a completely empty domain application.
NOTE: something about theexception this test raises messes up with the NOTE: something about theexception this test raises messes up with the
atomic block in a custom tearDown method for the parent test class.""" atomic block in a custom tearDown method for the parent test class."""
with less_console_noise():
with self.assertRaisesRegex(IntegrityError, "creator"): with self.assertRaisesRegex(IntegrityError, "creator"):
DomainApplication.objects.create() DomainApplication.objects.create()
def test_minimal_create(self): def test_minimal_create(self):
"""Can create with just a creator.""" """Can create with just a creator."""
with less_console_noise():
user, _ = User.objects.get_or_create(username="testy") user, _ = User.objects.get_or_create(username="testy")
application = DomainApplication.objects.create(creator=user) application = DomainApplication.objects.create(creator=user)
self.assertEqual(application.status, DomainApplication.ApplicationStatus.STARTED) self.assertEqual(application.status, DomainApplication.ApplicationStatus.STARTED)
def test_full_create(self): def test_full_create(self):
"""Can create with all fields.""" """Can create with all fields."""
with less_console_noise():
user, _ = User.objects.get_or_create(username="testy") user, _ = User.objects.get_or_create(username="testy")
contact = Contact.objects.create() contact = Contact.objects.create()
com_website, _ = Website.objects.get_or_create(website="igorville.com") com_website, _ = Website.objects.get_or_create(website="igorville.com")
@ -107,6 +111,7 @@ class TestDomainApplication(TestCase):
def test_domain_info(self): def test_domain_info(self):
"""Can create domain info with all fields.""" """Can create domain info with all fields."""
with less_console_noise():
user, _ = User.objects.get_or_create(username="testy") user, _ = User.objects.get_or_create(username="testy")
contact = Contact.objects.create() contact = Contact.objects.create()
domain, _ = Domain.objects.get_or_create(name="igorville.gov") domain, _ = Domain.objects.get_or_create(name="igorville.gov")
@ -133,6 +138,7 @@ class TestDomainApplication(TestCase):
self.assertEqual(information.id, domain.domain_info.id) self.assertEqual(information.id, domain.domain_info.id)
def test_status_fsm_submit_fail(self): def test_status_fsm_submit_fail(self):
with less_console_noise():
user, _ = User.objects.get_or_create(username="testy") user, _ = User.objects.get_or_create(username="testy")
application = DomainApplication.objects.create(creator=user) application = DomainApplication.objects.create(creator=user)
@ -143,6 +149,7 @@ class TestDomainApplication(TestCase):
application.submit() application.submit()
def test_status_fsm_submit_succeed(self): def test_status_fsm_submit_succeed(self):
with less_console_noise():
user, _ = User.objects.get_or_create(username="testy") user, _ = User.objects.get_or_create(username="testy")
site = DraftDomain.objects.create(name="igorville.gov") site = DraftDomain.objects.create(name="igorville.gov")
application = DomainApplication.objects.create(creator=user, requested_domain=site) application = DomainApplication.objects.create(creator=user, requested_domain=site)
@ -154,33 +161,63 @@ class TestDomainApplication(TestCase):
application.submit() application.submit()
self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED) self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED)
def test_submit_sends_email(self): def check_email_sent(self, application, msg, action, expected_count):
"""Create an application and submit it and see if email was sent.""" """Check if an email was sent after performing an action."""
user, _ = User.objects.get_or_create(username="testy")
contact = Contact.objects.create(email="test@test.gov")
domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
application = DomainApplication.objects.create(
creator=user,
requested_domain=domain,
submitter=contact,
)
application.save()
with self.subTest(msg=msg, action=action):
with boto3_mocking.clients.handler_for("sesv2", self.mock_client): with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise(): with less_console_noise():
application.submit() # Perform the specified action
action_method = getattr(application, action)
action_method()
# check to see if an email was sent # Check if an email was sent
self.assertGreater( sent_emails = [
len(
[
email email
for email in MockSESClient.EMAILS_SENT for email in MockSESClient.EMAILS_SENT
if "test@test.gov" in email["kwargs"]["Destination"]["ToAddresses"] if "mayor@igorville.gov" in email["kwargs"]["Destination"]["ToAddresses"]
] ]
), self.assertEqual(len(sent_emails), expected_count)
0,
) def test_submit_from_started_sends_email(self):
msg = "Create an application and submit it and see if email was sent."
application = completed_application()
self.check_email_sent(application, msg, "submit", 1)
def test_submit_from_withdrawn_sends_email(self):
msg = "Create a withdrawn application and submit it and see if email was sent."
application = completed_application(status=DomainApplication.ApplicationStatus.WITHDRAWN)
self.check_email_sent(application, msg, "submit", 1)
def test_submit_from_action_needed_does_not_send_email(self):
msg = "Create an application with ACTION_NEEDED status and submit it, check if email was not sent."
application = completed_application(status=DomainApplication.ApplicationStatus.ACTION_NEEDED)
self.check_email_sent(application, msg, "submit", 0)
def test_submit_from_in_review_does_not_send_email(self):
msg = "Create a withdrawn application and submit it and see if email was sent."
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
self.check_email_sent(application, msg, "submit", 0)
def test_approve_sends_email(self):
msg = "Create an application and approve it and see if email was sent."
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
self.check_email_sent(application, msg, "approve", 1)
def test_withdraw_sends_email(self):
msg = "Create an application and withdraw it and see if email was sent."
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
self.check_email_sent(application, msg, "withdraw", 1)
def test_reject_sends_email(self):
msg = "Create an application and reject it and see if email was sent."
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
self.check_email_sent(application, msg, "reject", 1)
def test_reject_with_prejudice_does_not_send_email(self):
msg = "Create an application and reject it with prejudice and see if email was sent."
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
self.check_email_sent(application, msg, "reject_with_prejudice", 0)
def test_submit_transition_allowed(self): def test_submit_transition_allowed(self):
""" """
@ -268,7 +305,7 @@ class TestDomainApplication(TestCase):
(self.rejected_application, TransitionNotAllowed), (self.rejected_application, TransitionNotAllowed),
(self.ineligible_application, TransitionNotAllowed), (self.ineligible_application, TransitionNotAllowed),
] ]
with less_console_noise():
for application, exception_type in test_cases: for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type): with self.subTest(application=application, exception_type=exception_type):
try: try:
@ -286,7 +323,7 @@ class TestDomainApplication(TestCase):
(self.action_needed_application, TransitionNotAllowed), (self.action_needed_application, TransitionNotAllowed),
(self.withdrawn_application, TransitionNotAllowed), (self.withdrawn_application, TransitionNotAllowed),
] ]
with less_console_noise():
for application, exception_type in test_cases: for application, exception_type in test_cases:
with self.subTest(application=application, exception_type=exception_type): with self.subTest(application=application, exception_type=exception_type):
with self.assertRaises(exception_type): with self.assertRaises(exception_type):
@ -457,6 +494,46 @@ class TestDomainApplication(TestCase):
with self.assertRaises(exception_type): with self.assertRaises(exception_type):
application.reject_with_prejudice() application.reject_with_prejudice()
def test_transition_not_allowed_approved_in_review_when_domain_is_active(self):
"""Create an application with status approved, create a matching domain that
is active, and call in_review against transition rules"""
domain = Domain.objects.create(name=self.approved_application.requested_domain.name)
self.approved_application.approved_domain = domain
self.approved_application.save()
# Define a custom implementation for is_active
def custom_is_active(self):
return True # Override to return True
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
# Use patch to temporarily replace is_active with the custom implementation
with patch.object(Domain, "is_active", custom_is_active):
# Now, when you call is_active on Domain, it will return True
with self.assertRaises(TransitionNotAllowed):
self.approved_application.in_review()
def test_transition_not_allowed_approved_action_needed_when_domain_is_active(self):
"""Create an application with status approved, create a matching domain that
is active, and call action_needed against transition rules"""
domain = Domain.objects.create(name=self.approved_application.requested_domain.name)
self.approved_application.approved_domain = domain
self.approved_application.save()
# Define a custom implementation for is_active
def custom_is_active(self):
return True # Override to return True
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
# Use patch to temporarily replace is_active with the custom implementation
with patch.object(Domain, "is_active", custom_is_active):
# Now, when you call is_active on Domain, it will return True
with self.assertRaises(TransitionNotAllowed):
self.approved_application.action_needed()
def test_transition_not_allowed_approved_rejected_when_domain_is_active(self): def test_transition_not_allowed_approved_rejected_when_domain_is_active(self):
"""Create an application with status approved, create a matching domain that """Create an application with status approved, create a matching domain that
is active, and call reject against transition rules""" is active, and call reject against transition rules"""
@ -499,21 +576,25 @@ class TestDomainApplication(TestCase):
def test_has_rationale_returns_true(self): def test_has_rationale_returns_true(self):
"""has_rationale() returns true when an application has no_other_contacts_rationale""" """has_rationale() returns true when an application has no_other_contacts_rationale"""
with less_console_noise():
self.started_application.no_other_contacts_rationale = "You talkin' to me?" self.started_application.no_other_contacts_rationale = "You talkin' to me?"
self.started_application.save() self.started_application.save()
self.assertEquals(self.started_application.has_rationale(), True) self.assertEquals(self.started_application.has_rationale(), True)
def test_has_rationale_returns_false(self): def test_has_rationale_returns_false(self):
"""has_rationale() returns false when an application has no no_other_contacts_rationale""" """has_rationale() returns false when an application has no no_other_contacts_rationale"""
with less_console_noise():
self.assertEquals(self.started_application.has_rationale(), False) self.assertEquals(self.started_application.has_rationale(), False)
def test_has_other_contacts_returns_true(self): def test_has_other_contacts_returns_true(self):
"""has_other_contacts() returns true when an application has other_contacts""" """has_other_contacts() returns true when an application has other_contacts"""
with less_console_noise():
# completed_application has other contacts by default # completed_application has other contacts by default
self.assertEquals(self.started_application.has_other_contacts(), True) self.assertEquals(self.started_application.has_other_contacts(), True)
def test_has_other_contacts_returns_false(self): def test_has_other_contacts_returns_false(self):
"""has_other_contacts() returns false when an application has no other_contacts""" """has_other_contacts() returns false when an application has no other_contacts"""
with less_console_noise():
application = completed_application( application = completed_application(
status=DomainApplication.ApplicationStatus.STARTED, name="no-others.gov", has_other_contacts=False status=DomainApplication.ApplicationStatus.STARTED, name="no-others.gov", has_other_contacts=False
) )
@ -549,7 +630,6 @@ class TestPermissions(TestCase):
class TestDomainInformation(TestCase): class TestDomainInformation(TestCase):
"""Test the DomainInformation model, when approved or otherwise""" """Test the DomainInformation model, when approved or otherwise"""
def setUp(self): def setUp(self):
@ -602,7 +682,6 @@ class TestDomainInformation(TestCase):
class TestInvitations(TestCase): class TestInvitations(TestCase):
"""Test the retrieval of invitations.""" """Test the retrieval of invitations."""
def setUp(self): def setUp(self):

View file

@ -3,10 +3,12 @@ Feature being tested: Registry Integration
This file tests the various ways in which the registrar interacts with the registry. This file tests the various ways in which the registrar interacts with the registry.
""" """
from django.test import TestCase from django.test import TestCase
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from unittest.mock import MagicMock, patch, call from unittest.mock import MagicMock, patch, call
import datetime import datetime
from django.utils.timezone import make_aware
from registrar.models import Domain, Host, HostIP from registrar.models import Domain, Host, HostIP
from unittest import skip from unittest import skip
@ -46,6 +48,7 @@ class TestDomainCache(MockEppLib):
def test_cache_sets_resets(self): def test_cache_sets_resets(self):
"""Cache should be set on getter and reset on setter calls""" """Cache should be set on getter and reset on setter calls"""
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="igorville.gov") domain, _ = Domain.objects.get_or_create(name="igorville.gov")
# trigger getter # trigger getter
_ = domain.creation_date _ = domain.creation_date
@ -75,6 +78,7 @@ class TestDomainCache(MockEppLib):
def test_cache_used_when_avail(self): def test_cache_used_when_avail(self):
"""Cache is pulled from if the object has already been accessed""" """Cache is pulled from if the object has already been accessed"""
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="igorville.gov") domain, _ = Domain.objects.get_or_create(name="igorville.gov")
cr_date = domain.creation_date cr_date = domain.creation_date
@ -94,6 +98,7 @@ class TestDomainCache(MockEppLib):
def test_cache_nested_elements(self): def test_cache_nested_elements(self):
"""Cache works correctly with the nested objects cache and hosts""" """Cache works correctly with the nested objects cache and hosts"""
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="igorville.gov") domain, _ = Domain.objects.get_or_create(name="igorville.gov")
# The contact list will initially contain objects of type 'DomainContact' # The contact list will initially contain objects of type 'DomainContact'
# this is then transformed into PublicContact, and cache should NOT # this is then transformed into PublicContact, and cache should NOT
@ -144,6 +149,7 @@ class TestDomainCache(MockEppLib):
def test_map_epp_contact_to_public_contact(self): def test_map_epp_contact_to_public_contact(self):
# Tests that the mapper is working how we expect # Tests that the mapper is working how we expect
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="registry.gov") domain, _ = Domain.objects.get_or_create(name="registry.gov")
security = PublicContact.ContactTypeChoices.SECURITY security = PublicContact.ContactTypeChoices.SECURITY
mapped = domain.map_epp_contact_to_public_contact( mapped = domain.map_epp_contact_to_public_contact(
@ -206,6 +212,7 @@ class TestDomainCache(MockEppLib):
gets invalid data from EPPLib gets invalid data from EPPLib
Then the function throws the expected ContactErrors Then the function throws the expected ContactErrors
""" """
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="registry.gov") domain, _ = Domain.objects.get_or_create(name="registry.gov")
fakedEpp = self.fakedEppObject() fakedEpp = self.fakedEppObject()
invalid_length = fakedEpp.dummyInfoContactResultData( invalid_length = fakedEpp.dummyInfoContactResultData(
@ -346,6 +353,7 @@ class TestDomainStatuses(MockEppLib):
def test_get_status(self): def test_get_status(self):
"""Domain 'statuses' getter returns statuses by calling epp""" """Domain 'statuses' getter returns statuses by calling epp"""
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="chicken-liver.gov") domain, _ = Domain.objects.get_or_create(name="chicken-liver.gov")
# trigger getter # trigger getter
_ = domain.statuses _ = domain.statuses
@ -365,6 +373,7 @@ class TestDomainStatuses(MockEppLib):
def test_get_status_returns_empty_list_when_value_error(self): def test_get_status_returns_empty_list_when_value_error(self):
"""Domain 'statuses' getter returns an empty list """Domain 'statuses' getter returns an empty list
when value error""" when value error"""
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="pig-knuckles.gov") domain, _ = Domain.objects.get_or_create(name="pig-knuckles.gov")
def side_effect(self): def side_effect(self):
@ -398,26 +407,21 @@ class TestDomainStatuses(MockEppLib):
first_ready is set when a domain is first transitioned to READY. It does not get overwritten first_ready is set when a domain is first transitioned to READY. It does not get overwritten
in case the domain gets out of and back into READY. in case the domain gets out of and back into READY.
""" """
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="pig-knuckles.gov", state=Domain.State.DNS_NEEDED) domain, _ = Domain.objects.get_or_create(name="pig-knuckles.gov", state=Domain.State.DNS_NEEDED)
self.assertEqual(domain.first_ready, None) self.assertEqual(domain.first_ready, None)
domain.ready() domain.ready()
# check that status is READY # check that status is READY
self.assertTrue(domain.is_active()) self.assertTrue(domain.is_active())
self.assertNotEqual(domain.first_ready, None) self.assertNotEqual(domain.first_ready, None)
# Capture the value of first_ready # Capture the value of first_ready
first_ready = domain.first_ready first_ready = domain.first_ready
# change domain status # change domain status
domain.dns_needed() domain.dns_needed()
self.assertFalse(domain.is_active()) self.assertFalse(domain.is_active())
# change back to READY # change back to READY
domain.ready() domain.ready()
self.assertTrue(domain.is_active()) self.assertTrue(domain.is_active())
# assert that the value of first_ready has not changed # assert that the value of first_ready has not changed
self.assertEqual(domain.first_ready, first_ready) self.assertEqual(domain.first_ready, first_ready)
@ -557,13 +561,11 @@ class TestRegistrantContacts(MockEppLib):
Then the domain has a valid security contact with CISA defaults Then the domain has a valid security contact with CISA defaults
And disclose flags are set to keep the email address hidden And disclose flags are set to keep the email address hidden
""" """
with less_console_noise():
# making a domain should make it domain # making a domain should make it domain
expectedSecContact = PublicContact.get_default_security() expectedSecContact = PublicContact.get_default_security()
expectedSecContact.domain = self.domain expectedSecContact.domain = self.domain
self.domain.dns_needed_from_unknown() self.domain.dns_needed_from_unknown()
self.assertEqual(self.mockedSendFunction.call_count, 8) self.assertEqual(self.mockedSendFunction.call_count, 8)
self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 4) self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 4)
self.assertEqual( self.assertEqual(
@ -573,19 +575,16 @@ class TestRegistrantContacts(MockEppLib):
).email, ).email,
expectedSecContact.email, expectedSecContact.email,
) )
id = PublicContact.objects.get( id = PublicContact.objects.get(
domain=self.domain, domain=self.domain,
contact_type=PublicContact.ContactTypeChoices.SECURITY, contact_type=PublicContact.ContactTypeChoices.SECURITY,
).registry_id ).registry_id
expectedSecContact.registry_id = id expectedSecContact.registry_id = id
expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=False) expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=False)
expectedUpdateDomain = commands.UpdateDomain( expectedUpdateDomain = commands.UpdateDomain(
name=self.domain.name, name=self.domain.name,
add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")], add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")],
) )
self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True)
self.mockedSendFunction.assert_any_call(expectedUpdateDomain, cleaned=True) self.mockedSendFunction.assert_any_call(expectedUpdateDomain, cleaned=True)
@ -598,6 +597,7 @@ class TestRegistrantContacts(MockEppLib):
And Domain sends `commands.UpdateDomain` to the registry with the newly And Domain sends `commands.UpdateDomain` to the registry with the newly
created contact of type 'security' created contact of type 'security'
""" """
with less_console_noise():
# make a security contact that is a PublicContact # make a security contact that is a PublicContact
# make sure a security email already exists # make sure a security email already exists
self.domain.dns_needed_from_unknown() self.domain.dns_needed_from_unknown()
@ -606,24 +606,19 @@ class TestRegistrantContacts(MockEppLib):
expectedSecContact.email = "newEmail@fake.com" expectedSecContact.email = "newEmail@fake.com"
expectedSecContact.registry_id = "456" expectedSecContact.registry_id = "456"
expectedSecContact.name = "Fakey McFakerson" expectedSecContact.name = "Fakey McFakerson"
# calls the security contact setter as if you did # calls the security contact setter as if you did
# self.domain.security_contact=expectedSecContact # self.domain.security_contact=expectedSecContact
expectedSecContact.save() expectedSecContact.save()
# no longer the default email it should be disclosed # no longer the default email it should be disclosed
expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=True) expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=True)
expectedUpdateDomain = commands.UpdateDomain( expectedUpdateDomain = commands.UpdateDomain(
name=self.domain.name, name=self.domain.name,
add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")], add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")],
) )
# check that send has triggered the create command for the contact # check that send has triggered the create command for the contact
receivedSecurityContact = PublicContact.objects.get( receivedSecurityContact = PublicContact.objects.get(
domain=self.domain, contact_type=PublicContact.ContactTypeChoices.SECURITY domain=self.domain, contact_type=PublicContact.ContactTypeChoices.SECURITY
) )
self.assertEqual(receivedSecurityContact, expectedSecContact) self.assertEqual(receivedSecurityContact, expectedSecContact)
self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True)
self.mockedSendFunction.assert_any_call(expectedUpdateDomain, cleaned=True) self.mockedSendFunction.assert_any_call(expectedUpdateDomain, cleaned=True)
@ -635,15 +630,12 @@ class TestRegistrantContacts(MockEppLib):
to the registry twice with identical data to the registry twice with identical data
Then no errors are raised in Domain Then no errors are raised in Domain
""" """
with less_console_noise():
security_contact = self.domain.get_default_security_contact() security_contact = self.domain.get_default_security_contact()
security_contact.registry_id = "fail" security_contact.registry_id = "fail"
security_contact.save() security_contact.save()
self.domain.security_contact = security_contact self.domain.security_contact = security_contact
expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=False) expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=False)
expectedUpdateDomain = commands.UpdateDomain( expectedUpdateDomain = commands.UpdateDomain(
name=self.domain.name, name=self.domain.name,
add=[common.DomainContact(contact=security_contact.registry_id, type="security")], add=[common.DomainContact(contact=security_contact.registry_id, type="security")],
@ -667,8 +659,8 @@ class TestRegistrantContacts(MockEppLib):
And the domain has a valid security contact with CISA defaults And the domain has a valid security contact with CISA defaults
And disclose flags are set to keep the email address hidden And disclose flags are set to keep the email address hidden
""" """
with less_console_noise():
old_contact = self.domain.get_default_security_contact() old_contact = self.domain.get_default_security_contact()
old_contact.registry_id = "fail" old_contact.registry_id = "fail"
old_contact.email = "user.entered@email.com" old_contact.email = "user.entered@email.com"
old_contact.save() old_contact.save()
@ -676,7 +668,6 @@ class TestRegistrantContacts(MockEppLib):
new_contact.registry_id = "fail" new_contact.registry_id = "fail"
new_contact.email = "" new_contact.email = ""
self.domain.security_contact = new_contact self.domain.security_contact = new_contact
firstCreateContactCall = self._convertPublicContactToEpp(old_contact, disclose_email=True) firstCreateContactCall = self._convertPublicContactToEpp(old_contact, disclose_email=True)
updateDomainAddCall = commands.UpdateDomain( updateDomainAddCall = commands.UpdateDomain(
name=self.domain.name, name=self.domain.name,
@ -692,7 +683,6 @@ class TestRegistrantContacts(MockEppLib):
name=self.domain.name, name=self.domain.name,
rem=[common.DomainContact(contact=old_contact.registry_id, type="security")], rem=[common.DomainContact(contact=old_contact.registry_id, type="security")],
) )
defaultSecID = PublicContact.objects.filter(domain=self.domain).get().registry_id defaultSecID = PublicContact.objects.filter(domain=self.domain).get().registry_id
default_security = PublicContact.get_default_security() default_security = PublicContact.get_default_security()
default_security.registry_id = defaultSecID default_security.registry_id = defaultSecID
@ -701,7 +691,6 @@ class TestRegistrantContacts(MockEppLib):
name=self.domain.name, name=self.domain.name,
add=[common.DomainContact(contact=defaultSecID, type="security")], add=[common.DomainContact(contact=defaultSecID, type="security")],
) )
expected_calls = [ expected_calls = [
call(firstCreateContactCall, cleaned=True), call(firstCreateContactCall, cleaned=True),
call(updateDomainAddCall, cleaned=True), call(updateDomainAddCall, cleaned=True),
@ -710,7 +699,6 @@ class TestRegistrantContacts(MockEppLib):
call(createDefaultContact, cleaned=True), call(createDefaultContact, cleaned=True),
call(updateDomainWDefault, cleaned=True), call(updateDomainWDefault, cleaned=True),
] ]
self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True)
def test_updates_security_email(self): def test_updates_security_email(self):
@ -721,12 +709,12 @@ class TestRegistrantContacts(MockEppLib):
security contact email security contact email
Then Domain sends `commands.UpdateContact` to the registry Then Domain sends `commands.UpdateContact` to the registry
""" """
with less_console_noise():
security_contact = self.domain.get_default_security_contact() security_contact = self.domain.get_default_security_contact()
security_contact.email = "originalUserEmail@gmail.com" security_contact.email = "originalUserEmail@gmail.com"
security_contact.registry_id = "fail" security_contact.registry_id = "fail"
security_contact.save() security_contact.save()
expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=True) expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=True)
expectedUpdateDomain = commands.UpdateDomain( expectedUpdateDomain = commands.UpdateDomain(
name=self.domain.name, name=self.domain.name,
add=[common.DomainContact(contact=security_contact.registry_id, type="security")], add=[common.DomainContact(contact=security_contact.registry_id, type="security")],
@ -735,7 +723,6 @@ class TestRegistrantContacts(MockEppLib):
security_contact.save() security_contact.save()
expectedSecondCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=True) expectedSecondCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=True)
updateContact = self._convertPublicContactToEpp(security_contact, disclose_email=True, createContact=False) updateContact = self._convertPublicContactToEpp(security_contact, disclose_email=True, createContact=False)
expected_calls = [ expected_calls = [
call(expectedCreateCommand, cleaned=True), call(expectedCreateCommand, cleaned=True),
call(expectedUpdateDomain, cleaned=True), call(expectedUpdateDomain, cleaned=True),
@ -751,8 +738,8 @@ class TestRegistrantContacts(MockEppLib):
Registry is unavailable and throws exception when attempting to build cache from Registry is unavailable and throws exception when attempting to build cache from
registry. Security email retrieved from database. registry. Security email retrieved from database.
""" """
with less_console_noise():
# Use self.domain_contact which has been initialized with existing contacts, including securityContact # Use self.domain_contact which has been initialized with existing contacts, including securityContact
# call get_security_email to initially set the security_contact_registry_id in the domain model # call get_security_email to initially set the security_contact_registry_id in the domain model
self.domain_contact.get_security_email() self.domain_contact.get_security_email()
# invalidate the cache so the next time get_security_email is called, it has to attempt to populate cache # invalidate the cache so the next time get_security_email is called, it has to attempt to populate cache
@ -765,11 +752,9 @@ class TestRegistrantContacts(MockEppLib):
patcher = patch("registrar.models.domain.registry.send") patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start() mocked_send = patcher.start()
mocked_send.side_effect = side_effect mocked_send.side_effect = side_effect
# when get_security_email is called, the registry error will force the security contact # when get_security_email is called, the registry error will force the security contact
# to be retrieved using the security_contact_registry_id in the domain model # to be retrieved using the security_contact_registry_id in the domain model
security_email = self.domain_contact.get_security_email() security_email = self.domain_contact.get_security_email()
# assert that the proper security contact was retrieved by testing the email matches expected value # assert that the proper security contact was retrieved by testing the email matches expected value
self.assertEqual(security_email, "security@mail.gov") self.assertEqual(security_email, "security@mail.gov")
patcher.stop() patcher.stop()
@ -781,6 +766,7 @@ class TestRegistrantContacts(MockEppLib):
The mocked data for the EPP calls for the freeman.gov domain returns a security The mocked data for the EPP calls for the freeman.gov domain returns a security
contact with registry id of securityContact when InfoContact is called contact with registry id of securityContact when InfoContact is called
""" """
with less_console_noise():
# Use self.domain_contact which has been initialized with existing contacts, including securityContact # Use self.domain_contact which has been initialized with existing contacts, including securityContact
# force fetch_cache to be called, which will return above documented mocked hosts # force fetch_cache to be called, which will return above documented mocked hosts
@ -798,54 +784,45 @@ class TestRegistrantContacts(MockEppLib):
And the field `disclose` is set to false for DF.EMAIL And the field `disclose` is set to false for DF.EMAIL
on all fields except security on all fields except security
""" """
with less_console_noise():
# Generates a domain with four existing contacts # Generates a domain with four existing contacts
domain, _ = Domain.objects.get_or_create(name="freeman.gov") domain, _ = Domain.objects.get_or_create(name="freeman.gov")
# Contact setup # Contact setup
expected_admin = domain.get_default_administrative_contact() expected_admin = domain.get_default_administrative_contact()
expected_admin.email = self.mockAdministrativeContact.email expected_admin.email = self.mockAdministrativeContact.email
expected_registrant = domain.get_default_registrant_contact() expected_registrant = domain.get_default_registrant_contact()
expected_registrant.email = self.mockRegistrantContact.email expected_registrant.email = self.mockRegistrantContact.email
expected_security = domain.get_default_security_contact() expected_security = domain.get_default_security_contact()
expected_security.email = self.mockSecurityContact.email expected_security.email = self.mockSecurityContact.email
expected_tech = domain.get_default_technical_contact() expected_tech = domain.get_default_technical_contact()
expected_tech.email = self.mockTechnicalContact.email expected_tech.email = self.mockTechnicalContact.email
domain.administrative_contact = expected_admin domain.administrative_contact = expected_admin
domain.registrant_contact = expected_registrant domain.registrant_contact = expected_registrant
domain.security_contact = expected_security domain.security_contact = expected_security
domain.technical_contact = expected_tech domain.technical_contact = expected_tech
contacts = [ contacts = [
(expected_admin, domain.administrative_contact), (expected_admin, domain.administrative_contact),
(expected_registrant, domain.registrant_contact), (expected_registrant, domain.registrant_contact),
(expected_security, domain.security_contact), (expected_security, domain.security_contact),
(expected_tech, domain.technical_contact), (expected_tech, domain.technical_contact),
] ]
# Test for each contact # Test for each contact
for contact in contacts: for contact in contacts:
expected_contact = contact[0] expected_contact = contact[0]
actual_contact = contact[1] actual_contact = contact[1]
is_security = expected_contact.contact_type == "security" is_security = expected_contact.contact_type == "security"
expectedCreateCommand = self._convertPublicContactToEpp(expected_contact, disclose_email=is_security) expectedCreateCommand = self._convertPublicContactToEpp(expected_contact, disclose_email=is_security)
# Should only be disclosed if the type is security, as the email is valid # Should only be disclosed if the type is security, as the email is valid
self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True)
# The emails should match on both items # The emails should match on both items
self.assertEqual(expected_contact.email, actual_contact.email) self.assertEqual(expected_contact.email, actual_contact.email)
def test_convert_public_contact_to_epp(self): def test_convert_public_contact_to_epp(self):
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="freeman.gov") domain, _ = Domain.objects.get_or_create(name="freeman.gov")
dummy_contact = domain.get_default_security_contact() dummy_contact = domain.get_default_security_contact()
test_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=True).__dict__ test_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=True).__dict__
test_not_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=False).__dict__ test_not_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=False).__dict__
# Separated for linter # Separated for linter
disclose_email_field = {common.DiscloseField.EMAIL} disclose_email_field = {common.DiscloseField.EMAIL}
expected_disclose = { expected_disclose = {
@ -872,7 +849,6 @@ class TestRegistrantContacts(MockEppLib):
"vat": None, "vat": None,
"voice": "+1.8882820870", "voice": "+1.8882820870",
} }
# Separated for linter # Separated for linter
expected_not_disclose = { expected_not_disclose = {
"auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"),
@ -898,11 +874,9 @@ class TestRegistrantContacts(MockEppLib):
"vat": None, "vat": None,
"voice": "+1.8882820870", "voice": "+1.8882820870",
} }
# Set the ids equal, since this value changes # Set the ids equal, since this value changes
test_disclose["id"] = expected_disclose["id"] test_disclose["id"] = expected_disclose["id"]
test_not_disclose["id"] = expected_not_disclose["id"] test_not_disclose["id"] = expected_not_disclose["id"]
self.assertEqual(test_disclose, expected_disclose) self.assertEqual(test_disclose, expected_disclose)
self.assertEqual(test_not_disclose, expected_not_disclose) self.assertEqual(test_not_disclose, expected_not_disclose)
@ -913,14 +887,13 @@ class TestRegistrantContacts(MockEppLib):
Then Domain sends `commands.CreateContact` to the registry Then Domain sends `commands.CreateContact` to the registry
And the field `disclose` is set to false for DF.EMAIL And the field `disclose` is set to false for DF.EMAIL
""" """
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="defaultsecurity.gov") domain, _ = Domain.objects.get_or_create(name="defaultsecurity.gov")
expectedSecContact = PublicContact.get_default_security() expectedSecContact = PublicContact.get_default_security()
expectedSecContact.domain = domain expectedSecContact.domain = domain
expectedSecContact.registry_id = "defaultSec" expectedSecContact.registry_id = "defaultSec"
domain.security_contact = expectedSecContact domain.security_contact = expectedSecContact
expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=False) expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=False)
self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True)
# Confirm that we are getting a default email # Confirm that we are getting a default email
self.assertEqual(domain.security_contact.email, expectedSecContact.email) self.assertEqual(domain.security_contact.email, expectedSecContact.email)
@ -932,14 +905,13 @@ class TestRegistrantContacts(MockEppLib):
Then Domain sends `commands.CreateContact` to the registry Then Domain sends `commands.CreateContact` to the registry
And the field `disclose` is set to false for DF.EMAIL And the field `disclose` is set to false for DF.EMAIL
""" """
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="defaulttechnical.gov") domain, _ = Domain.objects.get_or_create(name="defaulttechnical.gov")
expectedTechContact = PublicContact.get_default_technical() expectedTechContact = PublicContact.get_default_technical()
expectedTechContact.domain = domain expectedTechContact.domain = domain
expectedTechContact.registry_id = "defaultTech" expectedTechContact.registry_id = "defaultTech"
domain.technical_contact = expectedTechContact domain.technical_contact = expectedTechContact
expectedCreateCommand = self._convertPublicContactToEpp(expectedTechContact, disclose_email=False) expectedCreateCommand = self._convertPublicContactToEpp(expectedTechContact, disclose_email=False)
self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True)
# Confirm that we are getting a default email # Confirm that we are getting a default email
self.assertEqual(domain.technical_contact.email, expectedTechContact.email) self.assertEqual(domain.technical_contact.email, expectedTechContact.email)
@ -952,14 +924,13 @@ class TestRegistrantContacts(MockEppLib):
Then Domain sends `commands.CreateContact` to the registry Then Domain sends `commands.CreateContact` to the registry
And the field `disclose` is set to true for DF.EMAIL And the field `disclose` is set to true for DF.EMAIL
""" """
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="igorville.gov") domain, _ = Domain.objects.get_or_create(name="igorville.gov")
expectedSecContact = PublicContact.get_default_security() expectedSecContact = PublicContact.get_default_security()
expectedSecContact.domain = domain expectedSecContact.domain = domain
expectedSecContact.email = "123@mail.gov" expectedSecContact.email = "123@mail.gov"
domain.security_contact = expectedSecContact domain.security_contact = expectedSecContact
expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=True) expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=True)
self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True)
# Confirm that we are getting the desired email # Confirm that we are getting the desired email
self.assertEqual(domain.security_contact.email, expectedSecContact.email) self.assertEqual(domain.security_contact.email, expectedSecContact.email)
@ -974,6 +945,7 @@ class TestRegistrantContacts(MockEppLib):
raise raise
def test_contact_getter_security(self): def test_contact_getter_security(self):
with less_console_noise():
security = PublicContact.ContactTypeChoices.SECURITY security = PublicContact.ContactTypeChoices.SECURITY
# Create prexisting object # Create prexisting object
expected_contact = self.domain.map_epp_contact_to_public_contact( expected_contact = self.domain.map_epp_contact_to_public_contact(
@ -981,17 +953,13 @@ class TestRegistrantContacts(MockEppLib):
contact_id="securityContact", contact_id="securityContact",
contact_type=security, contact_type=security,
) )
# Checks if we grabbed the correct PublicContact # Checks if we grabbed the correct PublicContact
self.assertEqual(self.domain_contact.security_contact.email, expected_contact.email) self.assertEqual(self.domain_contact.security_contact.email, expected_contact.email)
expected_contact_db = PublicContact.objects.filter( expected_contact_db = PublicContact.objects.filter(
registry_id=self.domain_contact.security_contact.registry_id, registry_id=self.domain_contact.security_contact.registry_id,
contact_type=security, contact_type=security,
).get() ).get()
self.assertEqual(self.domain_contact.security_contact, expected_contact_db) self.assertEqual(self.domain_contact.security_contact, expected_contact_db)
self.mockedSendFunction.assert_has_calls( self.mockedSendFunction.assert_has_calls(
[ [
call( call(
@ -1005,21 +973,19 @@ class TestRegistrantContacts(MockEppLib):
self.assertEqual(cache.get(security), "securityContact") self.assertEqual(cache.get(security), "securityContact")
def test_contact_getter_technical(self): def test_contact_getter_technical(self):
with less_console_noise():
technical = PublicContact.ContactTypeChoices.TECHNICAL technical = PublicContact.ContactTypeChoices.TECHNICAL
expected_contact = self.domain.map_epp_contact_to_public_contact( expected_contact = self.domain.map_epp_contact_to_public_contact(
self.mockTechnicalContact, self.mockTechnicalContact,
contact_id="technicalContact", contact_id="technicalContact",
contact_type=technical, contact_type=technical,
) )
self.assertEqual(self.domain_contact.technical_contact.email, expected_contact.email) self.assertEqual(self.domain_contact.technical_contact.email, expected_contact.email)
# Checks if we grab the correct PublicContact # Checks if we grab the correct PublicContact
expected_contact_db = PublicContact.objects.filter( expected_contact_db = PublicContact.objects.filter(
registry_id=self.domain_contact.technical_contact.registry_id, registry_id=self.domain_contact.technical_contact.registry_id,
contact_type=technical, contact_type=technical,
).get() ).get()
# Checks if we grab the correct PublicContact # Checks if we grab the correct PublicContact
self.assertEqual(self.domain_contact.technical_contact, expected_contact_db) self.assertEqual(self.domain_contact.technical_contact, expected_contact_db)
self.mockedSendFunction.assert_has_calls( self.mockedSendFunction.assert_has_calls(
@ -1035,20 +1001,18 @@ class TestRegistrantContacts(MockEppLib):
self.assertEqual(cache.get(technical), "technicalContact") self.assertEqual(cache.get(technical), "technicalContact")
def test_contact_getter_administrative(self): def test_contact_getter_administrative(self):
with less_console_noise():
administrative = PublicContact.ContactTypeChoices.ADMINISTRATIVE administrative = PublicContact.ContactTypeChoices.ADMINISTRATIVE
expected_contact = self.domain.map_epp_contact_to_public_contact( expected_contact = self.domain.map_epp_contact_to_public_contact(
self.mockAdministrativeContact, self.mockAdministrativeContact,
contact_id="adminContact", contact_id="adminContact",
contact_type=administrative, contact_type=administrative,
) )
self.assertEqual(self.domain_contact.administrative_contact.email, expected_contact.email) self.assertEqual(self.domain_contact.administrative_contact.email, expected_contact.email)
expected_contact_db = PublicContact.objects.filter( expected_contact_db = PublicContact.objects.filter(
registry_id=self.domain_contact.administrative_contact.registry_id, registry_id=self.domain_contact.administrative_contact.registry_id,
contact_type=administrative, contact_type=administrative,
).get() ).get()
# Checks if we grab the correct PublicContact # Checks if we grab the correct PublicContact
self.assertEqual(self.domain_contact.administrative_contact, expected_contact_db) self.assertEqual(self.domain_contact.administrative_contact, expected_contact_db)
self.mockedSendFunction.assert_has_calls( self.mockedSendFunction.assert_has_calls(
@ -1064,19 +1028,17 @@ class TestRegistrantContacts(MockEppLib):
self.assertEqual(cache.get(administrative), "adminContact") self.assertEqual(cache.get(administrative), "adminContact")
def test_contact_getter_registrant(self): def test_contact_getter_registrant(self):
with less_console_noise():
expected_contact = self.domain.map_epp_contact_to_public_contact( expected_contact = self.domain.map_epp_contact_to_public_contact(
self.mockRegistrantContact, self.mockRegistrantContact,
contact_id="regContact", contact_id="regContact",
contact_type=PublicContact.ContactTypeChoices.REGISTRANT, contact_type=PublicContact.ContactTypeChoices.REGISTRANT,
) )
self.assertEqual(self.domain_contact.registrant_contact.email, expected_contact.email) self.assertEqual(self.domain_contact.registrant_contact.email, expected_contact.email)
expected_contact_db = PublicContact.objects.filter( expected_contact_db = PublicContact.objects.filter(
registry_id=self.domain_contact.registrant_contact.registry_id, registry_id=self.domain_contact.registrant_contact.registry_id,
contact_type=PublicContact.ContactTypeChoices.REGISTRANT, contact_type=PublicContact.ContactTypeChoices.REGISTRANT,
).get() ).get()
# Checks if we grab the correct PublicContact # Checks if we grab the correct PublicContact
self.assertEqual(self.domain_contact.registrant_contact, expected_contact_db) self.assertEqual(self.domain_contact.registrant_contact, expected_contact_db)
self.mockedSendFunction.assert_has_calls( self.mockedSendFunction.assert_has_calls(
@ -1112,6 +1074,7 @@ class TestRegistrantNameservers(MockEppLib):
def test_get_nameserver_changes_success_deleted_vals(self): def test_get_nameserver_changes_success_deleted_vals(self):
"""Testing only deleting and no other changes""" """Testing only deleting and no other changes"""
with less_console_noise():
self.domain._cache["hosts"] = [ self.domain._cache["hosts"] = [
{"name": "ns1.example.com", "addrs": None}, {"name": "ns1.example.com", "addrs": None},
{"name": "ns2.example.com", "addrs": ["1.2.3.4"]}, {"name": "ns2.example.com", "addrs": ["1.2.3.4"]},
@ -1136,6 +1099,7 @@ class TestRegistrantNameservers(MockEppLib):
def test_get_nameserver_changes_success_updated_vals(self): def test_get_nameserver_changes_success_updated_vals(self):
"""Testing only updating no other changes""" """Testing only updating no other changes"""
with less_console_noise():
self.domain._cache["hosts"] = [ self.domain._cache["hosts"] = [
{"name": "ns3.my-nameserver.gov", "addrs": ["1.2.3.4"]}, {"name": "ns3.my-nameserver.gov", "addrs": ["1.2.3.4"]},
] ]
@ -1148,7 +1112,6 @@ class TestRegistrantNameservers(MockEppLib):
new_values, new_values,
oldNameservers, oldNameservers,
) = self.domain.getNameserverChanges(newChanges) ) = self.domain.getNameserverChanges(newChanges)
self.assertEqual(deleted_values, []) self.assertEqual(deleted_values, [])
self.assertEqual(updated_values, [("ns3.my-nameserver.gov", ["1.2.4.5"])]) self.assertEqual(updated_values, [("ns3.my-nameserver.gov", ["1.2.4.5"])])
self.assertEqual(new_values, {}) self.assertEqual(new_values, {})
@ -1158,6 +1121,7 @@ class TestRegistrantNameservers(MockEppLib):
) )
def test_get_nameserver_changes_success_new_vals(self): def test_get_nameserver_changes_success_new_vals(self):
with less_console_noise():
# Testing only creating no other changes # Testing only creating no other changes
self.domain._cache["hosts"] = [ self.domain._cache["hosts"] = [
{"name": "ns1.example.com", "addrs": None}, {"name": "ns1.example.com", "addrs": None},
@ -1193,11 +1157,10 @@ class TestRegistrantNameservers(MockEppLib):
And `domain.is_active` returns False And `domain.is_active` returns False
And domain.first_ready is null And domain.first_ready is null
""" """
with less_console_noise():
# set 1 nameserver # set 1 nameserver
nameserver = "ns1.my-nameserver.com" nameserver = "ns1.my-nameserver.com"
self.domain.nameservers = [(nameserver,)] self.domain.nameservers = [(nameserver,)]
# when we create a host, we should've updated at the same time # when we create a host, we should've updated at the same time
created_host = commands.CreateHost(nameserver) created_host = commands.CreateHost(nameserver)
update_domain_with_created = commands.UpdateDomain( update_domain_with_created = commands.UpdateDomain(
@ -1205,19 +1168,15 @@ class TestRegistrantNameservers(MockEppLib):
add=[common.HostObjSet([created_host.name])], add=[common.HostObjSet([created_host.name])],
rem=[], rem=[],
) )
# checking if commands were sent (commands have to be sent in order) # checking if commands were sent (commands have to be sent in order)
expectedCalls = [ expectedCalls = [
call(created_host, cleaned=True), call(created_host, cleaned=True),
call(update_domain_with_created, cleaned=True), call(update_domain_with_created, cleaned=True),
] ]
self.mockedSendFunction.assert_has_calls(expectedCalls) self.mockedSendFunction.assert_has_calls(expectedCalls)
# check that status is still NOT READY # check that status is still NOT READY
# as you have less than 2 nameservers # as you have less than 2 nameservers
self.assertFalse(self.domain.is_active()) self.assertFalse(self.domain.is_active())
self.assertEqual(self.domain.first_ready, None) self.assertEqual(self.domain.first_ready, None)
def test_user_adds_two_nameservers(self): def test_user_adds_two_nameservers(self):
@ -1230,14 +1189,12 @@ class TestRegistrantNameservers(MockEppLib):
And `domain.is_active` returns True And `domain.is_active` returns True
And domain.first_ready is not null And domain.first_ready is not null
""" """
with less_console_noise():
# set 2 nameservers # set 2 nameservers
self.domain.nameservers = [(self.nameserver1,), (self.nameserver2,)] self.domain.nameservers = [(self.nameserver1,), (self.nameserver2,)]
# when you create a host, you also have to update at same time # when you create a host, you also have to update at same time
created_host1 = commands.CreateHost(self.nameserver1) created_host1 = commands.CreateHost(self.nameserver1)
created_host2 = commands.CreateHost(self.nameserver2) created_host2 = commands.CreateHost(self.nameserver2)
update_domain_with_created = commands.UpdateDomain( update_domain_with_created = commands.UpdateDomain(
name=self.domain.name, name=self.domain.name,
add=[ add=[
@ -1245,7 +1202,6 @@ class TestRegistrantNameservers(MockEppLib):
], ],
rem=[], rem=[],
) )
infoDomain = commands.InfoDomain(name="my-nameserver.gov", auth_info=None) infoDomain = commands.InfoDomain(name="my-nameserver.gov", auth_info=None)
# checking if commands were sent (commands have to be sent in order) # checking if commands were sent (commands have to be sent in order)
expectedCalls = [ expectedCalls = [
@ -1254,7 +1210,6 @@ class TestRegistrantNameservers(MockEppLib):
call(created_host2, cleaned=True), call(created_host2, cleaned=True),
call(update_domain_with_created, cleaned=True), call(update_domain_with_created, cleaned=True),
] ]
self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True)
self.assertEqual(4, self.mockedSendFunction.call_count) self.assertEqual(4, self.mockedSendFunction.call_count)
# check that status is READY # check that status is READY
@ -1268,7 +1223,7 @@ class TestRegistrantNameservers(MockEppLib):
When `domain.nameservers` is set to an array of length 14 When `domain.nameservers` is set to an array of length 14
Then Domain raises a user-friendly error Then Domain raises a user-friendly error
""" """
with less_console_noise():
# set 13+ nameservers # set 13+ nameservers
nameserver1 = "ns1.cats-are-superior1.com" nameserver1 = "ns1.cats-are-superior1.com"
nameserver2 = "ns1.cats-are-superior2.com" nameserver2 = "ns1.cats-are-superior2.com"
@ -1315,7 +1270,7 @@ class TestRegistrantNameservers(MockEppLib):
to the registry to the registry
And `domain.is_active` returns True And `domain.is_active` returns True
""" """
with less_console_noise():
# Mock is set to return 3 nameservers on infodomain # Mock is set to return 3 nameservers on infodomain
self.domainWithThreeNS.nameservers = [(self.nameserver1,), (self.nameserver2,)] self.domainWithThreeNS.nameservers = [(self.nameserver1,), (self.nameserver2,)]
expectedCalls = [ expectedCalls = [
@ -1343,7 +1298,6 @@ class TestRegistrantNameservers(MockEppLib):
), ),
call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True), call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True),
] ]
self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True)
self.assertTrue(self.domainWithThreeNS.is_active()) self.assertTrue(self.domainWithThreeNS.is_active())
@ -1357,7 +1311,7 @@ class TestRegistrantNameservers(MockEppLib):
And `domain.is_active` returns False And `domain.is_active` returns False
""" """
with less_console_noise():
self.domainWithThreeNS.nameservers = [(self.nameserver1,)] self.domainWithThreeNS.nameservers = [(self.nameserver1,)]
expectedCalls = [ expectedCalls = [
call( call(
@ -1389,7 +1343,6 @@ class TestRegistrantNameservers(MockEppLib):
), ),
call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True), call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True),
] ]
self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True)
self.assertFalse(self.domainWithThreeNS.is_active()) self.assertFalse(self.domainWithThreeNS.is_active())
@ -1403,12 +1356,12 @@ class TestRegistrantNameservers(MockEppLib):
And `commands.UpdateDomain` is sent to add #4 and #5 plus remove #2 and #3 And `commands.UpdateDomain` is sent to add #4 and #5 plus remove #2 and #3
And `commands.DeleteHost` is sent to delete #2 and #3 And `commands.DeleteHost` is sent to delete #2 and #3
""" """
with less_console_noise():
self.domainWithThreeNS.nameservers = [ self.domainWithThreeNS.nameservers = [
(self.nameserver1,), (self.nameserver1,),
("ns1.cats-are-superior1.com",), ("ns1.cats-are-superior1.com",),
("ns1.cats-are-superior2.com",), ("ns1.cats-are-superior2.com",),
] ]
expectedCalls = [ expectedCalls = [
call( call(
commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None),
@ -1453,7 +1406,6 @@ class TestRegistrantNameservers(MockEppLib):
cleaned=True, cleaned=True,
), ),
] ]
self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True)
self.assertTrue(self.domainWithThreeNS.is_active()) self.assertTrue(self.domainWithThreeNS.is_active())
@ -1465,9 +1417,8 @@ class TestRegistrantNameservers(MockEppLib):
with a subdomain of the domain and no IP addresses with a subdomain of the domain and no IP addresses
Then Domain raises a user-friendly error Then Domain raises a user-friendly error
""" """
with less_console_noise():
dotgovnameserver = "my-nameserver.gov" dotgovnameserver = "my-nameserver.gov"
with self.assertRaises(NameserverError): with self.assertRaises(NameserverError):
self.domain.nameservers = [(dotgovnameserver,)] self.domain.nameservers = [(dotgovnameserver,)]
@ -1480,6 +1431,7 @@ class TestRegistrantNameservers(MockEppLib):
with a different IP address(es) with a different IP address(es)
Then `commands.UpdateHost` is sent to the registry Then `commands.UpdateHost` is sent to the registry
""" """
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="nameserverwithip.gov", state=Domain.State.READY) domain, _ = Domain.objects.get_or_create(name="nameserverwithip.gov", state=Domain.State.READY)
domain.nameservers = [ domain.nameservers = [
("ns1.nameserverwithip.gov", ["2.3.4.5", "1.2.3.4"]), ("ns1.nameserverwithip.gov", ["2.3.4.5", "1.2.3.4"]),
@ -1489,7 +1441,6 @@ class TestRegistrantNameservers(MockEppLib):
), ),
("ns3.nameserverwithip.gov", ["2.3.4.5"]), ("ns3.nameserverwithip.gov", ["2.3.4.5"]),
] ]
expectedCalls = [ expectedCalls = [
call( call(
commands.InfoDomain(name="nameserverwithip.gov", auth_info=None), commands.InfoDomain(name="nameserverwithip.gov", auth_info=None),
@ -1517,7 +1468,6 @@ class TestRegistrantNameservers(MockEppLib):
cleaned=True, cleaned=True,
), ),
] ]
self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True)
self.assertTrue(domain.is_active()) self.assertTrue(domain.is_active())
@ -1529,8 +1479,8 @@ class TestRegistrantNameservers(MockEppLib):
which is not a subdomain of the domain and has IP addresses which is not a subdomain of the domain and has IP addresses
Then Domain raises a user-friendly error Then Domain raises a user-friendly error
""" """
with less_console_noise():
dotgovnameserver = "mynameserverdotgov.gov" dotgovnameserver = "mynameserverdotgov.gov"
with self.assertRaises(NameserverError): with self.assertRaises(NameserverError):
self.domain.nameservers = [(dotgovnameserver, ["1.2.3"])] self.domain.nameservers = [(dotgovnameserver, ["1.2.3"])]
@ -1541,14 +1491,13 @@ class TestRegistrantNameservers(MockEppLib):
to the registry twice with identical data to the registry twice with identical data
Then no errors are raised in Domain Then no errors are raised in Domain
""" """
with less_console_noise():
# Checking that it doesn't create or update even if out of order # Checking that it doesn't create or update even if out of order
self.domainWithThreeNS.nameservers = [ self.domainWithThreeNS.nameservers = [
(self.nameserver3,), (self.nameserver3,),
(self.nameserver1,), (self.nameserver1,),
(self.nameserver2,), (self.nameserver2,),
] ]
expectedCalls = [ expectedCalls = [
call( call(
commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None),
@ -1558,13 +1507,12 @@ class TestRegistrantNameservers(MockEppLib):
call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True), call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True),
call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True), call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True),
] ]
self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True)
self.assertEqual(self.mockedSendFunction.call_count, 4) self.assertEqual(self.mockedSendFunction.call_count, 4)
def test_is_subdomain_with_no_ip(self): def test_is_subdomain_with_no_ip(self):
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY) domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY)
with self.assertRaises(NameserverError): with self.assertRaises(NameserverError):
domain.nameservers = [ domain.nameservers = [
("ns1.nameserversubdomain.gov",), ("ns1.nameserversubdomain.gov",),
@ -1572,8 +1520,8 @@ class TestRegistrantNameservers(MockEppLib):
] ]
def test_not_subdomain_but_has_ip(self): def test_not_subdomain_but_has_ip(self):
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY) domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY)
with self.assertRaises(NameserverError): with self.assertRaises(NameserverError):
domain.nameservers = [ domain.nameservers = [
("ns1.cats-da-best.gov", ["1.2.3.4"]), ("ns1.cats-da-best.gov", ["1.2.3.4"]),
@ -1581,6 +1529,7 @@ class TestRegistrantNameservers(MockEppLib):
] ]
def test_is_subdomain_but_ip_addr_not_valid(self): def test_is_subdomain_but_ip_addr_not_valid(self):
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY) domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY)
with self.assertRaises(NameserverError): with self.assertRaises(NameserverError):
@ -1592,6 +1541,7 @@ class TestRegistrantNameservers(MockEppLib):
def test_setting_not_allowed(self): 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""" then setting nameservers is not allowed"""
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="onholdDomain.gov", state=Domain.State.ON_HOLD) domain, _ = Domain.objects.get_or_create(name="onholdDomain.gov", state=Domain.State.ON_HOLD)
with self.assertRaises(ActionNotAllowed): with self.assertRaises(ActionNotAllowed):
domain.nameservers = [self.nameserver1, self.nameserver2] domain.nameservers = [self.nameserver1, self.nameserver2]
@ -1602,6 +1552,7 @@ class TestRegistrantNameservers(MockEppLib):
Registry is unavailable and throws exception when attempting to build cache from Registry is unavailable and throws exception when attempting to build cache from
registry. Nameservers retrieved from database. registry. Nameservers retrieved from database.
""" """
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
# set the host and host_ips directly in the database; this is normally handled through # set the host and host_ips directly in the database; this is normally handled through
# fetch_cache # fetch_cache
@ -1609,20 +1560,16 @@ class TestRegistrantNameservers(MockEppLib):
host_ip, _ = HostIP.objects.get_or_create(host=host, address="1.1.1.1") host_ip, _ = HostIP.objects.get_or_create(host=host, address="1.1.1.1")
# mock that registry throws an error on the InfoHost send # mock that registry throws an error on the InfoHost send
def side_effect(_request, cleaned): def side_effect(_request, cleaned):
raise RegistryError(code=ErrorCode.COMMAND_FAILED) raise RegistryError(code=ErrorCode.COMMAND_FAILED)
patcher = patch("registrar.models.domain.registry.send") patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start() mocked_send = patcher.start()
mocked_send.side_effect = side_effect mocked_send.side_effect = side_effect
nameservers = domain.nameservers nameservers = domain.nameservers
self.assertEqual(len(nameservers), 1) self.assertEqual(len(nameservers), 1)
self.assertEqual(nameservers[0][0], "ns1.fake.gov") self.assertEqual(nameservers[0][0], "ns1.fake.gov")
self.assertEqual(nameservers[0][1], ["1.1.1.1"]) self.assertEqual(nameservers[0][1], ["1.1.1.1"])
patcher.stop() patcher.stop()
def test_nameservers_stored_on_fetch_cache(self): def test_nameservers_stored_on_fetch_cache(self):
@ -1633,8 +1580,8 @@ class TestRegistrantNameservers(MockEppLib):
of 'fake.host.com' from InfoDomain and an array of 2 IPs: 1.2.3.4 and 2.3.4.5 of 'fake.host.com' from InfoDomain and an array of 2 IPs: 1.2.3.4 and 2.3.4.5
from InfoHost from InfoHost
""" """
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
# mock the get_or_create methods for Host and HostIP # mock the get_or_create methods for Host and HostIP
with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object( with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object(
HostIP.objects, "get_or_create" HostIP.objects, "get_or_create"
@ -1642,7 +1589,6 @@ class TestRegistrantNameservers(MockEppLib):
# Set the return value for the mocks # Set the return value for the mocks
mock_host_get_or_create.return_value = (Host(), True) mock_host_get_or_create.return_value = (Host(), True)
mock_host_ip_get_or_create.return_value = (HostIP(), True) mock_host_ip_get_or_create.return_value = (HostIP(), True)
# force fetch_cache to be called, which will return above documented mocked hosts # force fetch_cache to be called, which will return above documented mocked hosts
domain.nameservers domain.nameservers
# assert that the mocks are called # assert that the mocks are called
@ -1791,13 +1737,12 @@ class TestRegistrantDNSSEC(MockEppLib):
else: else:
return MagicMock(res_data=[self.mockDataInfoHosts]) return MagicMock(res_data=[self.mockDataInfoHosts])
with less_console_noise():
patcher = patch("registrar.models.domain.registry.send") patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start() mocked_send = patcher.start()
mocked_send.side_effect = side_effect mocked_send.side_effect = side_effect
domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov")
domain.dnssecdata = self.dnssecExtensionWithDsData domain.dnssecdata = self.dnssecExtensionWithDsData
# get the DNS SEC extension added to the UpdateDomain command and # get the DNS SEC extension added to the UpdateDomain command and
# verify that it is properly sent # verify that it is properly sent
# args[0] is the _request sent to registry # args[0] is the _request sent to registry
@ -1835,9 +1780,7 @@ class TestRegistrantDNSSEC(MockEppLib):
), ),
] ]
) )
self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData) self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData)
patcher.stop() patcher.stop()
def test_dnssec_is_idempotent(self): def test_dnssec_is_idempotent(self):
@ -1872,12 +1815,11 @@ class TestRegistrantDNSSEC(MockEppLib):
else: else:
return MagicMock(res_data=[self.mockDataInfoHosts]) return MagicMock(res_data=[self.mockDataInfoHosts])
with less_console_noise():
patcher = patch("registrar.models.domain.registry.send") patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start() mocked_send = patcher.start()
mocked_send.side_effect = side_effect mocked_send.side_effect = side_effect
domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov")
# set the dnssecdata once # set the dnssecdata once
domain.dnssecdata = self.dnssecExtensionWithDsData domain.dnssecdata = self.dnssecExtensionWithDsData
# set the dnssecdata again # set the dnssecdata again
@ -1916,9 +1858,7 @@ class TestRegistrantDNSSEC(MockEppLib):
), ),
] ]
) )
self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData) self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData)
patcher.stop() patcher.stop()
def test_user_adds_dnssec_data_multiple_dsdata(self): def test_user_adds_dnssec_data_multiple_dsdata(self):
@ -1949,12 +1889,11 @@ class TestRegistrantDNSSEC(MockEppLib):
else: else:
return MagicMock(res_data=[self.mockDataInfoHosts]) return MagicMock(res_data=[self.mockDataInfoHosts])
with less_console_noise():
patcher = patch("registrar.models.domain.registry.send") patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start() mocked_send = patcher.start()
mocked_send.side_effect = side_effect mocked_send.side_effect = side_effect
domain, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov") domain, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov")
domain.dnssecdata = self.dnssecExtensionWithMultDsData domain.dnssecdata = self.dnssecExtensionWithMultDsData
# get the DNS SEC extension added to the UpdateDomain command # get the DNS SEC extension added to the UpdateDomain command
# and verify that it is properly sent # and verify that it is properly sent
@ -1987,9 +1926,7 @@ class TestRegistrantDNSSEC(MockEppLib):
), ),
] ]
) )
self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithMultDsData.dsData) self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithMultDsData.dsData)
patcher.stop() patcher.stop()
def test_user_removes_dnssec_data(self): def test_user_removes_dnssec_data(self):
@ -2021,10 +1958,10 @@ class TestRegistrantDNSSEC(MockEppLib):
else: else:
return MagicMock(res_data=[self.mockDataInfoHosts]) return MagicMock(res_data=[self.mockDataInfoHosts])
with less_console_noise():
patcher = patch("registrar.models.domain.registry.send") patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start() mocked_send = patcher.start()
mocked_send.side_effect = side_effect mocked_send.side_effect = side_effect
domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov")
# dnssecdata_get_initial = domain.dnssecdata # call to force initial mock # dnssecdata_get_initial = domain.dnssecdata # call to force initial mock
# domain._invalidate_cache() # domain._invalidate_cache()
@ -2078,7 +2015,6 @@ class TestRegistrantDNSSEC(MockEppLib):
), ),
] ]
) )
patcher.stop() patcher.stop()
def test_update_is_unsuccessful(self): def test_update_is_unsuccessful(self):
@ -2087,9 +2023,8 @@ class TestRegistrantDNSSEC(MockEppLib):
When an error is returned from epplibwrapper When an error is returned from epplibwrapper
Then a user-friendly error message is returned for displaying on the web Then a user-friendly error message is returned for displaying on the web
""" """
with less_console_noise():
domain, _ = Domain.objects.get_or_create(name="dnssec-invalid.gov") domain, _ = Domain.objects.get_or_create(name="dnssec-invalid.gov")
with self.assertRaises(RegistryError) as err: with self.assertRaises(RegistryError) as err:
domain.dnssecdata = self.dnssecExtensionWithDsData domain.dnssecdata = self.dnssecExtensionWithDsData
self.assertTrue(err.is_client_error() or err.is_session_error() or err.is_server_error()) self.assertTrue(err.is_client_error() or err.is_session_error() or err.is_server_error())
@ -2117,11 +2052,13 @@ class TestExpirationDate(MockEppLib):
def test_expiration_date_setter_not_implemented(self): def test_expiration_date_setter_not_implemented(self):
"""assert that the setter for expiration date is not implemented and will raise error""" """assert that the setter for expiration date is not implemented and will raise error"""
with less_console_noise():
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
self.domain.registry_expiration_date = datetime.date.today() self.domain.registry_expiration_date = datetime.date.today()
def test_renew_domain(self): def test_renew_domain(self):
"""assert that the renew_domain sets new expiration date in cache and saves to registrar""" """assert that the renew_domain sets new expiration date in cache and saves to registrar"""
with less_console_noise():
self.domain.renew_domain() self.domain.renew_domain()
test_date = datetime.date(2023, 5, 25) test_date = datetime.date(2023, 5, 25)
self.assertEquals(self.domain._cache["ex_date"], test_date) self.assertEquals(self.domain._cache["ex_date"], test_date)
@ -2129,28 +2066,31 @@ class TestExpirationDate(MockEppLib):
def test_renew_domain_error(self): def test_renew_domain_error(self):
"""assert that the renew_domain raises an exception when registry raises error""" """assert that the renew_domain raises an exception when registry raises error"""
with less_console_noise():
with self.assertRaises(RegistryError): with self.assertRaises(RegistryError):
self.domain_w_error.renew_domain() self.domain_w_error.renew_domain()
def test_is_expired(self): def test_is_expired(self):
"""assert that is_expired returns true for expiration_date in past""" """assert that is_expired returns true for expiration_date in past"""
with less_console_noise():
# force fetch_cache to be called # force fetch_cache to be called
self.domain.statuses self.domain.statuses
self.assertTrue(self.domain.is_expired) self.assertTrue(self.domain.is_expired)
def test_is_not_expired(self): def test_is_not_expired(self):
"""assert that is_expired returns false for expiration in future""" """assert that is_expired returns false for expiration in future"""
with less_console_noise():
# to do this, need to mock value returned from timezone.now # to do this, need to mock value returned from timezone.now
# set now to 2023-01-01 # set now to 2023-01-01
mocked_datetime = datetime.datetime(2023, 1, 1, 12, 0, 0) mocked_datetime = datetime.datetime(2023, 1, 1, 12, 0, 0)
# force fetch_cache which sets the expiration date to 2023-05-25 # force fetch_cache which sets the expiration date to 2023-05-25
self.domain.statuses self.domain.statuses
with patch("registrar.models.domain.timezone.now", return_value=mocked_datetime): with patch("registrar.models.domain.timezone.now", return_value=mocked_datetime):
self.assertFalse(self.domain.is_expired()) self.assertFalse(self.domain.is_expired())
def test_expiration_date_updated_on_info_domain_call(self): def test_expiration_date_updated_on_info_domain_call(self):
"""assert that expiration date in db is updated on info domain call""" """assert that expiration date in db is updated on info domain call"""
with less_console_noise():
# force fetch_cache to be called # force fetch_cache to be called
self.domain.statuses self.domain.statuses
test_date = datetime.date(2023, 5, 25) test_date = datetime.date(2023, 5, 25)
@ -2169,7 +2109,7 @@ class TestCreationDate(MockEppLib):
self.domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) self.domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
# creation_date returned from mockDataInfoDomain with creation date: # creation_date returned from mockDataInfoDomain with creation date:
# cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35) # cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35)
self.creation_date = datetime.datetime(2023, 5, 25, 19, 45, 35) self.creation_date = make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35))
def tearDown(self): def tearDown(self):
Domain.objects.all().delete() Domain.objects.all().delete()
@ -2212,6 +2152,7 @@ class TestAnalystClientHold(MockEppLib):
When `domain.place_client_hold()` is called When `domain.place_client_hold()` is called
Then `CLIENT_HOLD` is added to the domain's statuses Then `CLIENT_HOLD` is added to the domain's statuses
""" """
with less_console_noise():
self.domain.place_client_hold() self.domain.place_client_hold()
self.mockedSendFunction.assert_has_calls( self.mockedSendFunction.assert_has_calls(
[ [
@ -2243,6 +2184,7 @@ class TestAnalystClientHold(MockEppLib):
When `domain.place_client_hold()` is called When `domain.place_client_hold()` is called
Then Domain returns normally (without error) Then Domain returns normally (without error)
""" """
with less_console_noise():
self.domain_on_hold.place_client_hold() self.domain_on_hold.place_client_hold()
self.mockedSendFunction.assert_has_calls( self.mockedSendFunction.assert_has_calls(
[ [
@ -2274,6 +2216,7 @@ class TestAnalystClientHold(MockEppLib):
When `domain.remove_client_hold()` is called When `domain.remove_client_hold()` is called
Then `CLIENT_HOLD` is no longer in the domain's statuses Then `CLIENT_HOLD` is no longer in the domain's statuses
""" """
with less_console_noise():
self.domain_on_hold.revert_client_hold() self.domain_on_hold.revert_client_hold()
self.mockedSendFunction.assert_has_calls( self.mockedSendFunction.assert_has_calls(
[ [
@ -2305,6 +2248,7 @@ class TestAnalystClientHold(MockEppLib):
When `domain.remove_client_hold()` is called When `domain.remove_client_hold()` is called
Then Domain returns normally (without error) Then Domain returns normally (without error)
""" """
with less_console_noise():
self.domain.revert_client_hold() self.domain.revert_client_hold()
self.mockedSendFunction.assert_has_calls( self.mockedSendFunction.assert_has_calls(
[ [
@ -2339,17 +2283,16 @@ class TestAnalystClientHold(MockEppLib):
def side_effect(_request, cleaned): def side_effect(_request, cleaned):
raise RegistryError(code=ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION) raise RegistryError(code=ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION)
with less_console_noise():
patcher = patch("registrar.models.domain.registry.send") patcher = patch("registrar.models.domain.registry.send")
mocked_send = patcher.start() mocked_send = patcher.start()
mocked_send.side_effect = side_effect mocked_send.side_effect = side_effect
# if RegistryError is raised, admin formats user-friendly # if RegistryError is raised, admin formats user-friendly
# error message if error is_client_error, is_session_error, or # error message if error is_client_error, is_session_error, or
# is_server_error; so test for those conditions # is_server_error; so test for those conditions
with self.assertRaises(RegistryError) as err: with self.assertRaises(RegistryError) as err:
self.domain.place_client_hold() self.domain.place_client_hold()
self.assertTrue(err.is_client_error() or err.is_session_error() or err.is_server_error()) self.assertTrue(err.is_client_error() or err.is_session_error() or err.is_server_error())
patcher.stop() patcher.stop()
@ -2443,6 +2386,7 @@ class TestAnalystDelete(MockEppLib):
The deleted date is set. The deleted date is set.
""" """
with less_console_noise():
# Put the domain in client hold # Put the domain in client hold
self.domain.place_client_hold() self.domain.place_client_hold()
# Delete it... # Delete it...
@ -2456,16 +2400,12 @@ class TestAnalystDelete(MockEppLib):
) )
] ]
) )
# Domain itself should not be deleted # Domain itself should not be deleted
self.assertNotEqual(self.domain, None) self.assertNotEqual(self.domain, None)
# Domain should have the right state # Domain should have the right state
self.assertEqual(self.domain.state, Domain.State.DELETED) self.assertEqual(self.domain.state, Domain.State.DELETED)
# Domain should have a deleted # Domain should have a deleted
self.assertNotEqual(self.domain.deleted, None) self.assertNotEqual(self.domain.deleted, None)
# Cache should be invalidated # Cache should be invalidated
self.assertEqual(self.domain._cache, {}) self.assertEqual(self.domain._cache, {})
@ -2476,11 +2416,11 @@ class TestAnalystDelete(MockEppLib):
Then a client error is returned of code 2305 Then a client error is returned of code 2305
And `state` is not set to `DELETED` And `state` is not set to `DELETED`
""" """
with less_console_noise():
# Desired domain # Desired domain
domain, _ = Domain.objects.get_or_create(name="failDelete.gov", state=Domain.State.ON_HOLD) domain, _ = Domain.objects.get_or_create(name="failDelete.gov", state=Domain.State.ON_HOLD)
# Put the domain in client hold # Put the domain in client hold
domain.place_client_hold() domain.place_client_hold()
# Delete it # Delete it
with self.assertRaises(RegistryError) as err: with self.assertRaises(RegistryError) as err:
domain.deletedInEpp() domain.deletedInEpp()
@ -2494,7 +2434,6 @@ class TestAnalystDelete(MockEppLib):
) )
] ]
) )
# Domain itself should not be deleted # Domain itself should not be deleted
self.assertNotEqual(domain, None) self.assertNotEqual(domain, None)
# State should not have changed # State should not have changed
@ -2511,6 +2450,7 @@ class TestAnalystDelete(MockEppLib):
The deleted date is still null. The deleted date is still null.
""" """
with less_console_noise():
self.assertEqual(self.domain.state, Domain.State.READY) self.assertEqual(self.domain.state, Domain.State.READY)
with self.assertRaises(TransitionNotAllowed) as err: with self.assertRaises(TransitionNotAllowed) as err:
self.domain.deletedInEpp() self.domain.deletedInEpp()
@ -2520,6 +2460,5 @@ class TestAnalystDelete(MockEppLib):
self.assertNotEqual(self.domain, None) self.assertNotEqual(self.domain, None)
# Domain should have the right state # Domain should have the right state
self.assertEqual(self.domain.state, Domain.State.READY) self.assertEqual(self.domain.state, Domain.State.READY)
# deleted should be null # deleted should be null
self.assertEqual(self.domain.deleted, None) self.assertEqual(self.domain.deleted, None)

View file

@ -7,13 +7,14 @@ from registrar.models.domain import Domain
from registrar.models.public_contact import PublicContact from registrar.models.public_contact import PublicContact
from registrar.models.user import User from registrar.models.user import User
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from registrar.models.user_domain_role import UserDomainRole
from registrar.tests.common import MockEppLib from registrar.tests.common import MockEppLib
from registrar.utility.csv_export import ( from registrar.utility.csv_export import (
write_header, write_csv,
write_body,
get_default_start_date, get_default_start_date,
get_default_end_date, get_default_end_date,
) )
from django.core.management import call_command from django.core.management import call_command
from unittest.mock import MagicMock, call, mock_open, patch from unittest.mock import MagicMock, call, mock_open, patch
from api.views import get_current_federal, get_current_full from api.views import get_current_federal, get_current_full
@ -23,6 +24,7 @@ import boto3_mocking
from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # type: ignore from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # type: ignore
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from django.utils import timezone from django.utils import timezone
from .common import less_console_noise
class CsvReportsTest(TestCase): class CsvReportsTest(TestCase):
@ -80,6 +82,7 @@ class CsvReportsTest(TestCase):
@boto3_mocking.patching @boto3_mocking.patching
def test_generate_federal_report(self): def test_generate_federal_report(self):
"""Ensures that we correctly generate current-federal.csv""" """Ensures that we correctly generate current-federal.csv"""
with less_console_noise():
mock_client = MagicMock() mock_client = MagicMock()
fake_open = mock_open() fake_open = mock_open()
expected_file_content = [ expected_file_content = [
@ -99,6 +102,7 @@ class CsvReportsTest(TestCase):
@boto3_mocking.patching @boto3_mocking.patching
def test_generate_full_report(self): def test_generate_full_report(self):
"""Ensures that we correctly generate current-full.csv""" """Ensures that we correctly generate current-full.csv"""
with less_console_noise():
mock_client = MagicMock() mock_client = MagicMock()
fake_open = mock_open() fake_open = mock_open()
expected_file_content = [ expected_file_content = [
@ -123,6 +127,7 @@ class CsvReportsTest(TestCase):
def side_effect(Bucket, Key): def side_effect(Bucket, Key):
raise ClientError({"Error": {"Code": "NoSuchKey", "Message": "No such key"}}, "get_object") raise ClientError({"Error": {"Code": "NoSuchKey", "Message": "No such key"}}, "get_object")
with less_console_noise():
mock_client = MagicMock() mock_client = MagicMock()
mock_client.get_object.side_effect = side_effect mock_client.get_object.side_effect = side_effect
@ -144,6 +149,7 @@ class CsvReportsTest(TestCase):
def side_effect(Bucket, Key): def side_effect(Bucket, Key):
raise ClientError({"Error": {"Code": "NoSuchKey", "Message": "No such key"}}, "get_object") raise ClientError({"Error": {"Code": "NoSuchKey", "Message": "No such key"}}, "get_object")
with less_console_noise():
mock_client = MagicMock() mock_client = MagicMock()
mock_client.get_object.side_effect = side_effect mock_client.get_object.side_effect = side_effect
@ -160,6 +166,7 @@ class CsvReportsTest(TestCase):
@boto3_mocking.patching @boto3_mocking.patching
def test_load_federal_report(self): def test_load_federal_report(self):
"""Tests the get_current_federal api endpoint""" """Tests the get_current_federal api endpoint"""
with less_console_noise():
mock_client = MagicMock() mock_client = MagicMock()
mock_client_instance = mock_client.return_value mock_client_instance = mock_client.return_value
@ -192,6 +199,7 @@ class CsvReportsTest(TestCase):
@boto3_mocking.patching @boto3_mocking.patching
def test_load_full_report(self): def test_load_full_report(self):
"""Tests the current-federal api link""" """Tests the current-federal api link"""
with less_console_noise():
mock_client = MagicMock() mock_client = MagicMock()
mock_client_instance = mock_client.return_value mock_client_instance = mock_client.return_value
@ -329,34 +337,48 @@ class ExportDataTest(MockEppLib):
federal_agency="Armed Forces Retirement Home", federal_agency="Armed Forces Retirement Home",
) )
meoward_user = get_user_model().objects.create(
username="meoward_username", first_name="first_meoward", last_name="last_meoward", email="meoward@rocks.com"
)
# Test for more than 1 domain manager
_, created = UserDomainRole.objects.get_or_create(
user=meoward_user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
)
_, created = UserDomainRole.objects.get_or_create(
user=self.user, domain=self.domain_1, role=UserDomainRole.Roles.MANAGER
)
# Test for just 1 domain manager
_, created = UserDomainRole.objects.get_or_create(
user=meoward_user, domain=self.domain_2, role=UserDomainRole.Roles.MANAGER
)
def tearDown(self): def tearDown(self):
PublicContact.objects.all().delete() PublicContact.objects.all().delete()
Domain.objects.all().delete() Domain.objects.all().delete()
DomainInformation.objects.all().delete() DomainInformation.objects.all().delete()
User.objects.all().delete() User.objects.all().delete()
UserDomainRole.objects.all().delete()
super().tearDown() super().tearDown()
def test_export_domains_to_writer_security_emails(self): def test_export_domains_to_writer_security_emails(self):
"""Test that export_domains_to_writer returns the """Test that export_domains_to_writer returns the
expected security email""" expected security email"""
with less_console_noise():
# Add security email information # Add security email information
self.domain_1.name = "defaultsecurity.gov" self.domain_1.name = "defaultsecurity.gov"
self.domain_1.save() self.domain_1.save()
# Invoke setter # Invoke setter
self.domain_1.security_contact self.domain_1.security_contact
# Invoke setter # Invoke setter
self.domain_2.security_contact self.domain_2.security_contact
# Invoke setter # Invoke setter
self.domain_3.security_contact self.domain_3.security_contact
# Create a CSV file in memory # Create a CSV file in memory
csv_file = StringIO() csv_file = StringIO()
writer = csv.writer(csv_file) writer = csv.writer(csv_file)
# Define columns, sort fields, and filter condition # Define columns, sort fields, and filter condition
columns = [ columns = [
"Domain name", "Domain name",
@ -379,18 +401,16 @@ class ExportDataTest(MockEppLib):
Domain.State.ON_HOLD, Domain.State.ON_HOLD,
], ],
} }
self.maxDiff = None self.maxDiff = None
# Call the export functions # Call the export functions
write_header(writer, columns) write_csv(
write_body(writer, columns, sort_fields, filter_condition) writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
)
# Reset the CSV file's position to the beginning # Reset the CSV file's position to the beginning
csv_file.seek(0) csv_file.seek(0)
# Read the content into a variable # Read the content into a variable
csv_content = csv_file.read() csv_content = csv_file.read()
# We expect READY domains, # We expect READY domains,
# sorted alphabetially by domain name # sorted alphabetially by domain name
expected_content = ( expected_content = (
@ -401,18 +421,17 @@ class ExportDataTest(MockEppLib):
"ddomain3.gov,Federal,Armed Forces Retirement Home,123@mail.gov,On hold,2023-05-25\n" "ddomain3.gov,Federal,Armed Forces Retirement Home,123@mail.gov,On hold,2023-05-25\n"
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready" "defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready"
) )
# Normalize line endings and remove commas, # Normalize line endings and remove commas,
# spaces and leading/trailing whitespace # spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.assertEqual(csv_content, expected_content) self.assertEqual(csv_content, expected_content)
def test_write_body(self): def test_write_csv(self):
"""Test that write_body returns the """Test that write_body returns the
existing domain, test that sort by domain name works, existing domain, test that sort by domain name works,
test that filter works""" test that filter works"""
with less_console_noise():
# Create a CSV file in memory # Create a CSV file in memory
csv_file = StringIO() csv_file = StringIO()
writer = csv.writer(csv_file) writer = csv.writer(csv_file)
@ -442,17 +461,14 @@ class ExportDataTest(MockEppLib):
Domain.State.ON_HOLD, Domain.State.ON_HOLD,
], ],
} }
# Call the export functions # Call the export functions
write_header(writer, columns) write_csv(
write_body(writer, columns, sort_fields, filter_condition) writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
)
# Reset the CSV file's position to the beginning # Reset the CSV file's position to the beginning
csv_file.seek(0) csv_file.seek(0)
# Read the content into a variable # Read the content into a variable
csv_content = csv_file.read() csv_content = csv_file.read()
# We expect READY domains, # We expect READY domains,
# sorted alphabetially by domain name # sorted alphabetially by domain name
expected_content = ( expected_content = (
@ -464,20 +480,18 @@ class ExportDataTest(MockEppLib):
"cdomain1.gov,Federal - Executive,World War I Centennial Commission,Ready\n" "cdomain1.gov,Federal - Executive,World War I Centennial Commission,Ready\n"
"ddomain3.gov,Federal,Armed Forces Retirement Home,On hold\n" "ddomain3.gov,Federal,Armed Forces Retirement Home,On hold\n"
) )
# Normalize line endings and remove commas, # Normalize line endings and remove commas,
# spaces and leading/trailing whitespace # spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.assertEqual(csv_content, expected_content) self.assertEqual(csv_content, expected_content)
def test_write_body_additional(self): def test_write_body_additional(self):
"""An additional test for filters and multi-column sort""" """An additional test for filters and multi-column sort"""
with less_console_noise():
# Create a CSV file in memory # Create a CSV file in memory
csv_file = StringIO() csv_file = StringIO()
writer = csv.writer(csv_file) writer = csv.writer(csv_file)
# Define columns, sort fields, and filter condition # Define columns, sort fields, and filter condition
columns = [ columns = [
"Domain name", "Domain name",
@ -497,17 +511,14 @@ class ExportDataTest(MockEppLib):
Domain.State.ON_HOLD, Domain.State.ON_HOLD,
], ],
} }
# Call the export functions # Call the export functions
write_header(writer, columns) write_csv(
write_body(writer, columns, sort_fields, filter_condition) writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True
)
# Reset the CSV file's position to the beginning # Reset the CSV file's position to the beginning
csv_file.seek(0) csv_file.seek(0)
# Read the content into a variable # Read the content into a variable
csv_content = csv_file.read() csv_content = csv_file.read()
# We expect READY domains, # We expect READY domains,
# federal only # federal only
# sorted alphabetially by domain name # sorted alphabetially by domain name
@ -518,12 +529,10 @@ class ExportDataTest(MockEppLib):
"cdomain1.gov,Federal - Executive,World War I Centennial Commission\n" "cdomain1.gov,Federal - Executive,World War I Centennial Commission\n"
"ddomain3.gov,Federal,Armed Forces Retirement Home\n" "ddomain3.gov,Federal,Armed Forces Retirement Home\n"
) )
# Normalize line endings and remove commas, # Normalize line endings and remove commas,
# spaces and leading/trailing whitespace # spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.assertEqual(csv_content, expected_content) self.assertEqual(csv_content, expected_content)
def test_write_body_with_date_filter_pulls_domains_in_range(self): def test_write_body_with_date_filter_pulls_domains_in_range(self):
@ -538,12 +547,12 @@ class ExportDataTest(MockEppLib):
which are hard to mock. which are hard to mock.
TODO: Simplify is created_at is not needed for the report.""" TODO: Simplify is created_at is not needed for the report."""
with less_console_noise():
# Create a CSV file in memory # Create a CSV file in memory
csv_file = StringIO() csv_file = StringIO()
writer = csv.writer(csv_file) writer = csv.writer(csv_file)
# We use timezone.make_aware to sync to server time a datetime object with the current date (using date.today()) # We use timezone.make_aware to sync to server time a datetime object with the current date
# and a specific time (using datetime.min.time()). # (using date.today()) and a specific time (using datetime.min.time()).
end_date = timezone.make_aware(datetime.combine(date.today() + timedelta(days=2), datetime.min.time())) end_date = timezone.make_aware(datetime.combine(date.today() + timedelta(days=2), datetime.min.time()))
start_date = timezone.make_aware(datetime.combine(date.today() - timedelta(days=2), datetime.min.time())) start_date = timezone.make_aware(datetime.combine(date.today() - timedelta(days=2), datetime.min.time()))
@ -582,20 +591,22 @@ class ExportDataTest(MockEppLib):
} }
# Call the export functions # Call the export functions
write_header(writer, columns) write_csv(
write_body(
writer, writer,
columns, columns,
sort_fields, sort_fields,
filter_condition, filter_condition,
get_domain_managers=False,
should_write_header=True,
) )
write_body( write_csv(
writer, writer,
columns, columns,
sort_fields_for_deleted_domains, sort_fields_for_deleted_domains,
filter_conditions_for_deleted_domains, filter_conditions_for_deleted_domains,
get_domain_managers=False,
should_write_header=False,
) )
# Reset the CSV file's position to the beginning # Reset the CSV file's position to the beginning
csv_file.seek(0) csv_file.seek(0)
@ -621,6 +632,64 @@ class ExportDataTest(MockEppLib):
self.assertEqual(csv_content, expected_content) self.assertEqual(csv_content, expected_content)
def test_export_domains_to_writer_domain_managers(self):
"""Test that export_domains_to_writer returns the
expected domain managers"""
with less_console_noise():
# Create a CSV file in memory
csv_file = StringIO()
writer = csv.writer(csv_file)
# Define columns, sort fields, and filter condition
columns = [
"Domain name",
"Status",
"Expiration date",
"Domain type",
"Agency",
"Organization name",
"City",
"State",
"AO",
"AO email",
"Security contact email",
]
sort_fields = ["domain__name"]
filter_condition = {
"domain__state__in": [
Domain.State.READY,
Domain.State.DNS_NEEDED,
Domain.State.ON_HOLD,
],
}
self.maxDiff = None
# Call the export functions
write_csv(
writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True
)
# Reset the CSV file's position to the beginning
csv_file.seek(0)
# Read the content into a variable
csv_content = csv_file.read()
# We expect READY domains,
# sorted alphabetially by domain name
expected_content = (
"Domain name,Status,Expiration date,Domain type,Agency,"
"Organization name,City,State,AO,AO email,"
"Security contact email,Domain manager email 1,Domain manager email 2,\n"
"adomain10.gov,Ready,,Federal,Armed Forces Retirement Home,,,, , ,\n"
"adomain2.gov,Dns needed,,Interstate,,,,, , , ,meoward@rocks.com\n"
"cdomain1.gov,Ready,,Federal - Executive,World War I Centennial Commission,,,"
", , , ,meoward@rocks.com,info@example.com\n"
"ddomain3.gov,On hold,,Federal,Armed Forces Retirement Home,,,, , , ,,\n"
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
self.assertEqual(csv_content, expected_content)
class HelperFunctions(TestCase): class HelperFunctions(TestCase):
"""This asserts that 1=1. Its limited usefulness lies in making sure the helper methods stay healthy.""" """This asserts that 1=1. Its limited usefulness lies in making sure the helper methods stay healthy."""

View file

@ -20,6 +20,9 @@ from registrar.models.contact import Contact
from .common import MockSESClient, less_console_noise from .common import MockSESClient, less_console_noise
import boto3_mocking # type: ignore import boto3_mocking # type: ignore
import logging
logger = logging.getLogger(__name__)
class TestProcessedMigrations(TestCase): class TestProcessedMigrations(TestCase):
@ -55,6 +58,7 @@ class TestProcessedMigrations(TestCase):
The 'call_command' function from Django's management framework is then used to The 'call_command' function from Django's management framework is then used to
execute the load_transition_domain command with the specified arguments. execute the load_transition_domain command with the specified arguments.
""" """
with less_console_noise():
# noqa here because splitting this up makes it confusing. # noqa here because splitting this up makes it confusing.
# ES501 # ES501
with patch( with patch(
@ -74,6 +78,7 @@ class TestProcessedMigrations(TestCase):
The 'call_command' function from Django's management framework is then used to The 'call_command' function from Django's management framework is then used to
execute the load_transition_domain command with the specified arguments. execute the load_transition_domain command with the specified arguments.
""" """
with less_console_noise():
call_command("transfer_transition_domains_to_domains") call_command("transfer_transition_domains_to_domains")
def test_domain_idempotent(self): def test_domain_idempotent(self):
@ -81,6 +86,7 @@ class TestProcessedMigrations(TestCase):
This test ensures that the domain transfer process This test ensures that the domain transfer process
is idempotent on Domain and DomainInformation. is idempotent on Domain and DomainInformation.
""" """
with less_console_noise():
unchanged_domain, _ = Domain.objects.get_or_create( unchanged_domain, _ = Domain.objects.get_or_create(
name="testdomain.gov", name="testdomain.gov",
state=Domain.State.READY, state=Domain.State.READY,
@ -139,6 +145,7 @@ class TestProcessedMigrations(TestCase):
""" """
This test checks if a domain is correctly marked as processed in the transition. This test checks if a domain is correctly marked as processed in the transition.
""" """
with less_console_noise():
old_transition_domain, _ = TransitionDomain.objects.get_or_create(domain_name="testdomain.gov") old_transition_domain, _ = TransitionDomain.objects.get_or_create(domain_name="testdomain.gov")
# Asser that old records default to 'True' # Asser that old records default to 'True'
self.assertTrue(old_transition_domain.processed) self.assertTrue(old_transition_domain.processed)
@ -200,6 +207,7 @@ class TestOrganizationMigration(TestCase):
The 'call_command' function from Django's management framework is then used to The 'call_command' function from Django's management framework is then used to
execute the load_transition_domain command with the specified arguments. execute the load_transition_domain command with the specified arguments.
""" """
with less_console_noise():
# noqa here because splitting this up makes it confusing. # noqa here because splitting this up makes it confusing.
# ES501 # ES501
with patch( with patch(
@ -219,6 +227,7 @@ class TestOrganizationMigration(TestCase):
The 'call_command' function from Django's management framework is then used to The 'call_command' function from Django's management framework is then used to
execute the load_transition_domain command with the specified arguments. execute the load_transition_domain command with the specified arguments.
""" """
with less_console_noise():
call_command("transfer_transition_domains_to_domains") call_command("transfer_transition_domains_to_domains")
def run_load_organization_data(self): def run_load_organization_data(self):
@ -232,6 +241,7 @@ class TestOrganizationMigration(TestCase):
The 'call_command' function from Django's management framework is then used to The 'call_command' function from Django's management framework is then used to
execute the load_organization_data command with the specified arguments. execute the load_organization_data command with the specified arguments.
""" """
with less_console_noise():
# noqa here (E501) because splitting this up makes it # noqa here (E501) because splitting this up makes it
# confusing to read. # confusing to read.
with patch( with patch(
@ -256,7 +266,6 @@ class TestOrganizationMigration(TestCase):
"""Does a diff between the transition_domain and the following tables: """Does a diff between the transition_domain and the following tables:
domain, domain_information and the domain_invitation. domain, domain_information and the domain_invitation.
Verifies that the data loaded correctly.""" Verifies that the data loaded correctly."""
missing_domains = [] missing_domains = []
duplicate_domains = [] duplicate_domains = []
missing_domain_informations = [] missing_domain_informations = []
@ -300,8 +309,11 @@ class TestOrganizationMigration(TestCase):
3. Checks that the data has been loaded as expected. 3. Checks that the data has been loaded as expected.
The expected result is a set of TransitionDomain objects with specific attributes. The expected result is a set of TransitionDomain objects with specific attributes.
The test fetches the actual TransitionDomain objects from the database and compares them with the expected objects. The test fetches the actual TransitionDomain objects from the database and compares them with
""" # noqa - E501 (harder to read) the expected objects.
"""
with less_console_noise():
# noqa - E501 (harder to read)
# == First, parse all existing data == # # == First, parse all existing data == #
self.run_load_domains() self.run_load_domains()
self.run_transfer_domains() self.run_transfer_domains()
@ -346,7 +358,9 @@ class TestOrganizationMigration(TestCase):
def test_transition_domain_status_unknown(self): def test_transition_domain_status_unknown(self):
""" """
Test that a domain in unknown status can be loaded Test that a domain in unknown status can be loaded
""" # noqa - E501 (harder to read) """
with less_console_noise():
# noqa - E501 (harder to read)
# == First, parse all existing data == # # == First, parse all existing data == #
self.run_load_domains() self.run_load_domains()
self.run_transfer_domains() self.run_transfer_domains()
@ -367,6 +381,7 @@ class TestOrganizationMigration(TestCase):
The test fetches the actual DomainInformation object from the database The test fetches the actual DomainInformation object from the database
and compares it with the expected object. and compares it with the expected object.
""" """
with less_console_noise():
# == First, parse all existing data == # # == First, parse all existing data == #
self.run_load_domains() self.run_load_domains()
self.run_transfer_domains() self.run_transfer_domains()
@ -379,7 +394,9 @@ class TestOrganizationMigration(TestCase):
domain_information = DomainInformation.objects.filter(domain=_domain).get() domain_information = DomainInformation.objects.filter(domain=_domain).get()
expected_creator = User.objects.filter(username="System").get() expected_creator = User.objects.filter(username="System").get()
expected_ao = Contact.objects.filter(first_name="Seline", middle_name="testmiddle2", last_name="Tower").get() expected_ao = Contact.objects.filter(
first_name="Seline", middle_name="testmiddle2", last_name="Tower"
).get()
expected_domain_information = DomainInformation( expected_domain_information = DomainInformation(
creator=expected_creator, creator=expected_creator,
organization_type="federal", organization_type="federal",
@ -410,6 +427,7 @@ class TestOrganizationMigration(TestCase):
The expected result is that the DomainInformation object retains its pre-existing data The expected result is that the DomainInformation object retains its pre-existing data
after the load_organization_data method is run. after the load_organization_data method is run.
""" """
with less_console_noise():
# == First, parse all existing data == # # == First, parse all existing data == #
self.run_load_domains() self.run_load_domains()
self.run_transfer_domains() self.run_transfer_domains()
@ -431,7 +449,9 @@ class TestOrganizationMigration(TestCase):
domain_information = DomainInformation.objects.filter(domain=_domain).get() domain_information = DomainInformation.objects.filter(domain=_domain).get()
expected_creator = User.objects.filter(username="System").get() expected_creator = User.objects.filter(username="System").get()
expected_ao = Contact.objects.filter(first_name="Seline", middle_name="testmiddle2", last_name="Tower").get() expected_ao = Contact.objects.filter(
first_name="Seline", middle_name="testmiddle2", last_name="Tower"
).get()
expected_domain_information = DomainInformation( expected_domain_information = DomainInformation(
creator=expected_creator, creator=expected_creator,
organization_type="federal", organization_type="federal",
@ -462,6 +482,7 @@ class TestOrganizationMigration(TestCase):
The expected result is that the counts of objects in the database The expected result is that the counts of objects in the database
match the expected counts, indicating that the data has not been corrupted. match the expected counts, indicating that the data has not been corrupted.
""" """
with less_console_noise():
# First, parse all existing data # First, parse all existing data
self.run_load_domains() self.run_load_domains()
self.run_transfer_domains() self.run_transfer_domains()
@ -521,6 +542,7 @@ class TestMigrations(TestCase):
UserDomainRole.objects.all().delete() UserDomainRole.objects.all().delete()
def run_load_domains(self): def run_load_domains(self):
with less_console_noise():
# noqa here because splitting this up makes it confusing. # noqa here because splitting this up makes it confusing.
# ES501 # ES501
with patch( with patch(
@ -534,9 +556,11 @@ class TestMigrations(TestCase):
) )
def run_transfer_domains(self): def run_transfer_domains(self):
with less_console_noise():
call_command("transfer_transition_domains_to_domains") call_command("transfer_transition_domains_to_domains")
def run_master_script(self): def run_master_script(self):
with less_console_noise():
# noqa here (E501) because splitting this up makes it # noqa here (E501) because splitting this up makes it
# confusing to read. # confusing to read.
mock_client = MockSESClient() mock_client = MockSESClient()
@ -553,7 +577,7 @@ class TestMigrations(TestCase):
migrationJSON=self.migration_json_filename, migrationJSON=self.migration_json_filename,
disablePrompts=True, disablePrompts=True,
) )
print(f"here: {mock_client.EMAILS_SENT}") logger.debug(f"here: {mock_client.EMAILS_SENT}")
def compare_tables( def compare_tables(
self, self,
@ -607,7 +631,7 @@ class TestMigrations(TestCase):
total_domain_informations = len(DomainInformation.objects.all()) total_domain_informations = len(DomainInformation.objects.all())
total_domain_invitations = len(DomainInvitation.objects.all()) total_domain_invitations = len(DomainInvitation.objects.all())
print( logger.debug(
f""" f"""
total_missing_domains = {len(missing_domains)} total_missing_domains = {len(missing_domains)}
total_duplicate_domains = {len(duplicate_domains)} total_duplicate_domains = {len(duplicate_domains)}
@ -636,7 +660,7 @@ class TestMigrations(TestCase):
follow best practice of limiting the number of assertions per test. follow best practice of limiting the number of assertions per test.
But for now, this will double-check that the script But for now, this will double-check that the script
works as intended.""" works as intended."""
with less_console_noise():
self.run_master_script() self.run_master_script()
# STEP 2: (analyze the tables just like the # STEP 2: (analyze the tables just like the
@ -664,6 +688,7 @@ class TestMigrations(TestCase):
def test_load_empty_transition_domain(self): def test_load_empty_transition_domain(self):
"""Loads TransitionDomains without additional data""" """Loads TransitionDomains without additional data"""
with less_console_noise():
self.run_load_domains() self.run_load_domains()
# STEP 2: (analyze the tables just like the migration # STEP 2: (analyze the tables just like the migration
@ -689,6 +714,7 @@ class TestMigrations(TestCase):
) )
def test_load_full_domain(self): def test_load_full_domain(self):
with less_console_noise():
self.run_load_domains() self.run_load_domains()
self.run_transfer_domains() self.run_transfer_domains()
@ -733,6 +759,7 @@ class TestMigrations(TestCase):
self.assertEqual(testdomain.state, "on hold") self.assertEqual(testdomain.state, "on hold")
def test_load_full_domain_information(self): def test_load_full_domain_information(self):
with less_console_noise():
self.run_load_domains() self.run_load_domains()
self.run_transfer_domains() self.run_transfer_domains()
@ -800,6 +827,7 @@ class TestMigrations(TestCase):
self.assertEqual(anomaly.creator, Users.get()) self.assertEqual(anomaly.creator, Users.get())
def test_transfer_transition_domains_to_domains(self): def test_transfer_transition_domains_to_domains(self):
with less_console_noise():
self.run_load_domains() self.run_load_domains()
self.run_transfer_domains() self.run_transfer_domains()
@ -825,6 +853,7 @@ class TestMigrations(TestCase):
) )
def test_logins(self): def test_logins(self):
with less_console_noise():
# TODO: setup manually instead of calling other scripts # TODO: setup manually instead of calling other scripts
self.run_load_domains() self.run_load_domains()
self.run_transfer_domains() self.run_transfer_domains()

View file

@ -114,6 +114,13 @@ class TestURLAuth(TestCase):
"/api/v1/available/", "/api/v1/available/",
"/api/v1/get-report/current-federal", "/api/v1/get-report/current-federal",
"/api/v1/get-report/current-full", "/api/v1/get-report/current-full",
"/health",
]
# We will test that the following URLs are not protected by auth
# and that the url returns a 200 response
NO_AUTH_URLS = [
"/health",
] ]
def assertURLIsProtectedByAuth(self, url): def assertURLIsProtectedByAuth(self, url):
@ -147,9 +154,33 @@ class TestURLAuth(TestCase):
f"GET {url} returned HTTP {code}, but should redirect to login or deny access", f"GET {url} returned HTTP {code}, but should redirect to login or deny access",
) )
def assertURLIsNotProtectedByAuth(self, url):
"""
Make a GET request to the given URL, and ensure that it returns 200.
"""
try:
with less_console_noise():
response = self.client.get(url)
except Exception as e:
# It'll be helpful to provide information on what URL was being
# accessed at the time the exception occurred. Python 3 will
# also include a full traceback of the original exception, so
# we don't need to worry about hiding the original cause.
raise AssertionError(f'Accessing {url} raised "{e}"', e)
code = response.status_code
if code != 200:
raise AssertionError(
f"GET {url} returned HTTP {code}, but should return 200 OK",
)
def test_login_required_all_urls(self): def test_login_required_all_urls(self):
"""All URLs redirect to the login view.""" """All URLs redirect to the login view."""
for viewname, url in iter_sample_urls(registrar.config.urls): for viewname, url in iter_sample_urls(registrar.config.urls):
if url not in self.IGNORE_URLS: if url not in self.IGNORE_URLS:
with self.subTest(viewname=viewname): with self.subTest(viewname=viewname):
self.assertURLIsProtectedByAuth(url) self.assertURLIsProtectedByAuth(url)
elif url in self.NO_AUTH_URLS:
with self.subTest(viewname=viewname):
self.assertURLIsNotProtectedByAuth(url)

View file

@ -1,5 +1,4 @@
from django.test import Client, TestCase from django.test import Client, TestCase
from django.urls import reverse
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from .common import MockEppLib # type: ignore from .common import MockEppLib # type: ignore
@ -8,11 +7,7 @@ from .common import MockEppLib # type: ignore
from registrar.models import ( from registrar.models import (
DomainApplication, DomainApplication,
DomainInformation, DomainInformation,
DraftDomain,
Contact,
User,
) )
from .common import less_console_noise
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -23,7 +18,7 @@ class TestViews(TestCase):
self.client = Client() self.client = Client()
def test_health_check_endpoint(self): def test_health_check_endpoint(self):
response = self.client.get("/health/") response = self.client.get("/health")
self.assertContains(response, "OK", status_code=200) self.assertContains(response, "OK", status_code=200)
def test_home_page(self): def test_home_page(self):
@ -55,252 +50,3 @@ class TestWithUser(MockEppLib):
DomainApplication.objects.all().delete() DomainApplication.objects.all().delete()
DomainInformation.objects.all().delete() DomainInformation.objects.all().delete()
self.user.delete() self.user.delete()
class LoggedInTests(TestWithUser):
def setUp(self):
super().setUp()
self.client.force_login(self.user)
def tearDown(self):
super().tearDown()
Contact.objects.all().delete()
def test_home_lists_domain_applications(self):
response = self.client.get("/")
self.assertNotContains(response, "igorville.gov")
site = DraftDomain.objects.create(name="igorville.gov")
application = DomainApplication.objects.create(creator=self.user, requested_domain=site)
response = self.client.get("/")
# count = 7 because of screenreader content
self.assertContains(response, "igorville.gov", count=7)
# clean up
application.delete()
def test_home_deletes_withdrawn_domain_application(self):
"""Tests if the user can delete a DomainApplication in the 'withdrawn' status"""
site = DraftDomain.objects.create(name="igorville.gov")
application = DomainApplication.objects.create(
creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.WITHDRAWN
)
# Ensure that igorville.gov exists on the page
home_page = self.client.get("/")
self.assertContains(home_page, "igorville.gov")
# Check if the delete button exists. We can do this by checking for its id and text content.
self.assertContains(home_page, "Delete")
self.assertContains(home_page, "button-toggle-delete-domain-alert-1")
# Trigger the delete logic
response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True)
self.assertNotContains(response, "igorville.gov")
# clean up
application.delete()
def test_home_deletes_started_domain_application(self):
"""Tests if the user can delete a DomainApplication in the 'started' status"""
site = DraftDomain.objects.create(name="igorville.gov")
application = DomainApplication.objects.create(
creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.STARTED
)
# Ensure that igorville.gov exists on the page
home_page = self.client.get("/")
self.assertContains(home_page, "igorville.gov")
# Check if the delete button exists. We can do this by checking for its id and text content.
self.assertContains(home_page, "Delete")
self.assertContains(home_page, "button-toggle-delete-domain-alert-1")
# Trigger the delete logic
response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True)
self.assertNotContains(response, "igorville.gov")
# clean up
application.delete()
def test_home_doesnt_delete_other_domain_applications(self):
"""Tests to ensure the user can't delete Applications not in the status of STARTED or WITHDRAWN"""
# Given that we are including a subset of items that can be deleted while excluding the rest,
# subTest is appropriate here as otherwise we would need many duplicate tests for the same reason.
draft_domain = DraftDomain.objects.create(name="igorville.gov")
for status in DomainApplication.ApplicationStatus:
if status not in [
DomainApplication.ApplicationStatus.STARTED,
DomainApplication.ApplicationStatus.WITHDRAWN,
]:
with self.subTest(status=status):
application = DomainApplication.objects.create(
creator=self.user, requested_domain=draft_domain, status=status
)
# Trigger the delete logic
response = self.client.post(
reverse("application-delete", kwargs={"pk": application.pk}), follow=True
)
# Check for a 403 error - the end user should not be allowed to do this
self.assertEqual(response.status_code, 403)
desired_application = DomainApplication.objects.filter(requested_domain=draft_domain)
# Make sure the DomainApplication wasn't deleted
self.assertEqual(desired_application.count(), 1)
# clean up
application.delete()
def test_home_deletes_domain_application_and_orphans(self):
"""Tests if delete for DomainApplication deletes orphaned Contact objects"""
# Create the site and contacts to delete (orphaned)
contact = Contact.objects.create(
first_name="Henry",
last_name="Mcfakerson",
)
contact_shared = Contact.objects.create(
first_name="Relative",
last_name="Aether",
)
# Create two non-orphaned contacts
contact_2 = Contact.objects.create(
first_name="Saturn",
last_name="Mars",
)
# Attach a user object to a contact (should not be deleted)
contact_user, _ = Contact.objects.get_or_create(user=self.user)
site = DraftDomain.objects.create(name="igorville.gov")
application = DomainApplication.objects.create(
creator=self.user,
requested_domain=site,
status=DomainApplication.ApplicationStatus.WITHDRAWN,
authorizing_official=contact,
submitter=contact_user,
)
application.other_contacts.set([contact_2])
# Create a second application to attach contacts to
site_2 = DraftDomain.objects.create(name="teaville.gov")
application_2 = DomainApplication.objects.create(
creator=self.user,
requested_domain=site_2,
status=DomainApplication.ApplicationStatus.STARTED,
authorizing_official=contact_2,
submitter=contact_shared,
)
application_2.other_contacts.set([contact_shared])
# Ensure that igorville.gov exists on the page
home_page = self.client.get("/")
self.assertContains(home_page, "igorville.gov")
# Trigger the delete logic
response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True)
# igorville is now deleted
self.assertNotContains(response, "igorville.gov")
# Check if the orphaned contact was deleted
orphan = Contact.objects.filter(id=contact.id)
self.assertFalse(orphan.exists())
# All non-orphan contacts should still exist and are unaltered
try:
current_user = Contact.objects.filter(id=contact_user.id).get()
except Contact.DoesNotExist:
self.fail("contact_user (a non-orphaned contact) was deleted")
self.assertEqual(current_user, contact_user)
try:
edge_case = Contact.objects.filter(id=contact_2.id).get()
except Contact.DoesNotExist:
self.fail("contact_2 (a non-orphaned contact) was deleted")
self.assertEqual(edge_case, contact_2)
def test_home_deletes_domain_application_and_shared_orphans(self):
"""Test the edge case for an object that will become orphaned after a delete
(but is not an orphan at the time of deletion)"""
# Create the site and contacts to delete (orphaned)
contact = Contact.objects.create(
first_name="Henry",
last_name="Mcfakerson",
)
contact_shared = Contact.objects.create(
first_name="Relative",
last_name="Aether",
)
# Create two non-orphaned contacts
contact_2 = Contact.objects.create(
first_name="Saturn",
last_name="Mars",
)
# Attach a user object to a contact (should not be deleted)
contact_user, _ = Contact.objects.get_or_create(user=self.user)
site = DraftDomain.objects.create(name="igorville.gov")
application = DomainApplication.objects.create(
creator=self.user,
requested_domain=site,
status=DomainApplication.ApplicationStatus.WITHDRAWN,
authorizing_official=contact,
submitter=contact_user,
)
application.other_contacts.set([contact_2])
# Create a second application to attach contacts to
site_2 = DraftDomain.objects.create(name="teaville.gov")
application_2 = DomainApplication.objects.create(
creator=self.user,
requested_domain=site_2,
status=DomainApplication.ApplicationStatus.STARTED,
authorizing_official=contact_2,
submitter=contact_shared,
)
application_2.other_contacts.set([contact_shared])
home_page = self.client.get("/")
self.assertContains(home_page, "teaville.gov")
# Trigger the delete logic
response = self.client.post(reverse("application-delete", kwargs={"pk": application_2.pk}), follow=True)
self.assertNotContains(response, "teaville.gov")
# Check if the orphaned contact was deleted
orphan = Contact.objects.filter(id=contact_shared.id)
self.assertFalse(orphan.exists())
def test_application_form_view(self):
response = self.client.get("/request/", follow=True)
self.assertContains(
response,
"Youre about to start your .gov domain request.",
)
def test_domain_application_form_with_ineligible_user(self):
"""Application form not accessible for an ineligible user.
This test should be solid enough since all application wizard
views share the same permissions class"""
self.user.status = User.RESTRICTED
self.user.save()
with less_console_noise():
response = self.client.get("/request/", follow=True)
print(response.status_code)
self.assertEqual(response.status_code, 403)

View file

@ -1,7 +1,9 @@
from unittest import skip from unittest import skip
from unittest.mock import Mock
from django.conf import settings from django.conf import settings
from django.urls import reverse from django.urls import reverse
from datetime import date
from .common import MockSESClient, completed_application # type: ignore from .common import MockSESClient, completed_application # type: ignore
from django_webtest import WebTest # type: ignore from django_webtest import WebTest # type: ignore
@ -9,11 +11,13 @@ import boto3_mocking # type: ignore
from registrar.models import ( from registrar.models import (
DomainApplication, DomainApplication,
DraftDomain,
Domain, Domain,
DomainInformation, DomainInformation,
Contact, Contact,
User, User,
Website, Website,
UserDomainRole,
) )
from registrar.views.application import ApplicationWizard, Step from registrar.views.application import ApplicationWizard, Step
@ -26,7 +30,6 @@ logger = logging.getLogger(__name__)
class DomainApplicationTests(TestWithUser, WebTest): class DomainApplicationTests(TestWithUser, WebTest):
"""Webtests for domain application to test filling and submitting.""" """Webtests for domain application to test filling and submitting."""
# Doesn't work with CSRF checking # Doesn't work with CSRF checking
@ -2197,3 +2200,492 @@ class DomainApplicationTestDifferentStatuses(TestWithUser, WebTest):
# domain object, so we do not expect to see 'city.gov' # domain object, so we do not expect to see 'city.gov'
# in either the Domains or Requests tables. # in either the Domains or Requests tables.
self.assertNotContains(home_page, "city.gov") self.assertNotContains(home_page, "city.gov")
class TestWizardUnlockingSteps(TestWithUser, WebTest):
def setUp(self):
super().setUp()
self.app.set_user(self.user.username)
self.wizard = ApplicationWizard()
# Mock the request object, its user, and session attributes appropriately
self.wizard.request = Mock(user=self.user, session={})
def tearDown(self):
super().tearDown()
def test_unlocked_steps_empty_application(self):
"""Test when all fields in the application are empty."""
unlocked_steps = self.wizard.db_check_for_unlocking_steps()
expected_dict = []
self.assertEqual(unlocked_steps, expected_dict)
def test_unlocked_steps_full_application(self):
"""Test when all fields in the application are filled."""
completed_application(status=DomainApplication.ApplicationStatus.STARTED, user=self.user)
# Make a request to the home page
home_page = self.app.get("/")
# django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept
# of a "session". We are going to do it manually, saving the session ID here
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Assert that the response contains "city.gov"
self.assertContains(home_page, "city.gov")
# Click the "Edit" link
response = home_page.click("Edit", index=0)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Check if the response is a redirect
if response.status_code == 302:
# Follow the redirect manually
try:
detail_page = response.follow()
self.wizard.get_context_data()
except Exception as err:
# Handle any potential errors while following the redirect
self.fail(f"Error following the redirect {err}")
# Now 'detail_page' contains the response after following the redirect
self.assertEqual(detail_page.status_code, 200)
# 10 unlocked steps, one active step, the review step will have link_usa but not check_circle
self.assertContains(detail_page, "#check_circle", count=10)
# Type of organization
self.assertContains(detail_page, "usa-current", count=1)
self.assertContains(detail_page, "link_usa-checked", count=11)
else:
self.fail(f"Expected a redirect, but got a different response: {response}")
def test_unlocked_steps_partial_application(self):
"""Test when some fields in the application are filled."""
# Create the site and contacts to delete (orphaned)
contact = Contact.objects.create(
first_name="Henry",
last_name="Mcfakerson",
)
# Create two non-orphaned contacts
contact_2 = Contact.objects.create(
first_name="Saturn",
last_name="Mars",
)
# Attach a user object to a contact (should not be deleted)
contact_user, _ = Contact.objects.get_or_create(user=self.user)
site = DraftDomain.objects.create(name="igorville.gov")
application = DomainApplication.objects.create(
creator=self.user,
requested_domain=site,
status=DomainApplication.ApplicationStatus.WITHDRAWN,
authorizing_official=contact,
submitter=contact_user,
)
application.other_contacts.set([contact_2])
# Make a request to the home page
home_page = self.app.get("/")
# django-webtest does not handle cookie-based sessions well because it keeps
# resetting the session key on each new request, thus destroying the concept
# of a "session". We are going to do it manually, saving the session ID here
# and then setting the cookie on each request.
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Assert that the response contains "city.gov"
self.assertContains(home_page, "igorville.gov")
# Click the "Edit" link
response = home_page.click("Edit", index=0)
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Check if the response is a redirect
if response.status_code == 302:
# Follow the redirect manually
try:
detail_page = response.follow()
self.wizard.get_context_data()
except Exception as err:
# Handle any potential errors while following the redirect
self.fail(f"Error following the redirect {err}")
# Now 'detail_page' contains the response after following the redirect
self.assertEqual(detail_page.status_code, 200)
# 5 unlocked steps (ao, domain, submitter, other contacts, and current sites
# which unlocks if domain exists), one active step, the review step is locked
self.assertContains(detail_page, "#check_circle", count=5)
# Type of organization
self.assertContains(detail_page, "usa-current", count=1)
self.assertContains(detail_page, "link_usa-checked", count=5)
else:
self.fail(f"Expected a redirect, but got a different response: {response}")
class HomeTests(TestWithUser):
"""A series of tests that target the two tables on home.html"""
def setUp(self):
super().setUp()
self.client.force_login(self.user)
def tearDown(self):
super().tearDown()
Contact.objects.all().delete()
def test_home_lists_domain_applications(self):
response = self.client.get("/")
self.assertNotContains(response, "igorville.gov")
site = DraftDomain.objects.create(name="igorville.gov")
application = DomainApplication.objects.create(creator=self.user, requested_domain=site)
response = self.client.get("/")
# count = 7 because of screenreader content
self.assertContains(response, "igorville.gov", count=7)
# clean up
application.delete()
def test_state_help_text(self):
"""Tests if each domain state has help text"""
# Get the expected text content of each state
deleted_text = "This domain has been removed and " "is no longer registered to your organization."
dns_needed_text = "Before this domain can be used, " "youll need to add name server addresses."
ready_text = "This domain has name servers and is ready for use."
on_hold_text = (
"This domain is administratively paused, "
"so it cant be edited and wont resolve in DNS. "
"Contact help@get.gov for details."
)
deleted_text = "This domain has been removed and " "is no longer registered to your organization."
# Generate a mapping of domain names, the state, and expected messages for the subtest
test_cases = [
("deleted.gov", Domain.State.DELETED, deleted_text),
("dnsneeded.gov", Domain.State.DNS_NEEDED, dns_needed_text),
("unknown.gov", Domain.State.UNKNOWN, dns_needed_text),
("onhold.gov", Domain.State.ON_HOLD, on_hold_text),
("ready.gov", Domain.State.READY, ready_text),
]
for domain_name, state, expected_message in test_cases:
with self.subTest(domain_name=domain_name, state=state, expected_message=expected_message):
# Create a domain and a UserRole with the given params
test_domain, _ = Domain.objects.get_or_create(name=domain_name, state=state)
test_domain.expiration_date = date.today()
test_domain.save()
user_role, _ = UserDomainRole.objects.get_or_create(
user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER
)
# Grab the home page
response = self.client.get("/")
# Make sure the user can actually see the domain.
# We expect two instances because of SR content.
self.assertContains(response, domain_name, count=2)
# Check that we have the right text content.
self.assertContains(response, expected_message, count=1)
# Delete the role and domain to ensure we're testing in isolation
user_role.delete()
test_domain.delete()
def test_state_help_text_expired(self):
"""Tests if each domain state has help text when expired"""
expired_text = "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov."
test_domain, _ = Domain.objects.get_or_create(name="expired.gov", state=Domain.State.READY)
test_domain.expiration_date = date(2011, 10, 10)
test_domain.save()
UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER)
# Grab the home page
response = self.client.get("/")
# Make sure the user can actually see the domain.
# We expect two instances because of SR content.
self.assertContains(response, "expired.gov", count=2)
# Check that we have the right text content.
self.assertContains(response, expired_text, count=1)
def test_state_help_text_no_expiration_date(self):
"""Tests if each domain state has help text when expiration date is None"""
# == Test a expiration of None for state ready. This should be expired. == #
expired_text = "This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov."
test_domain, _ = Domain.objects.get_or_create(name="imexpired.gov", state=Domain.State.READY)
test_domain.expiration_date = None
test_domain.save()
UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain, role=UserDomainRole.Roles.MANAGER)
# Grab the home page
response = self.client.get("/")
# Make sure the user can actually see the domain.
# We expect two instances because of SR content.
self.assertContains(response, "imexpired.gov", count=2)
# Make sure the expiration date is None
self.assertEqual(test_domain.expiration_date, None)
# Check that we have the right text content.
self.assertContains(response, expired_text, count=1)
# == Test a expiration of None for state unknown. This should not display expired text. == #
unknown_text = "Before this domain can be used, " "youll need to add name server addresses."
test_domain_2, _ = Domain.objects.get_or_create(name="notexpired.gov", state=Domain.State.UNKNOWN)
test_domain_2.expiration_date = None
test_domain_2.save()
UserDomainRole.objects.get_or_create(user=self.user, domain=test_domain_2, role=UserDomainRole.Roles.MANAGER)
# Grab the home page
response = self.client.get("/")
# Make sure the user can actually see the domain.
# We expect two instances because of SR content.
self.assertContains(response, "notexpired.gov", count=2)
# Make sure the expiration date is None
self.assertEqual(test_domain_2.expiration_date, None)
# Check that we have the right text content.
self.assertContains(response, unknown_text, count=1)
def test_home_deletes_withdrawn_domain_application(self):
"""Tests if the user can delete a DomainApplication in the 'withdrawn' status"""
site = DraftDomain.objects.create(name="igorville.gov")
application = DomainApplication.objects.create(
creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.WITHDRAWN
)
# Ensure that igorville.gov exists on the page
home_page = self.client.get("/")
self.assertContains(home_page, "igorville.gov")
# Check if the delete button exists. We can do this by checking for its id and text content.
self.assertContains(home_page, "Delete")
self.assertContains(home_page, "button-toggle-delete-domain-alert-1")
# Trigger the delete logic
response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True)
self.assertNotContains(response, "igorville.gov")
# clean up
application.delete()
def test_home_deletes_started_domain_application(self):
"""Tests if the user can delete a DomainApplication in the 'started' status"""
site = DraftDomain.objects.create(name="igorville.gov")
application = DomainApplication.objects.create(
creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.STARTED
)
# Ensure that igorville.gov exists on the page
home_page = self.client.get("/")
self.assertContains(home_page, "igorville.gov")
# Check if the delete button exists. We can do this by checking for its id and text content.
self.assertContains(home_page, "Delete")
self.assertContains(home_page, "button-toggle-delete-domain-alert-1")
# Trigger the delete logic
response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True)
self.assertNotContains(response, "igorville.gov")
# clean up
application.delete()
def test_home_doesnt_delete_other_domain_applications(self):
"""Tests to ensure the user can't delete Applications not in the status of STARTED or WITHDRAWN"""
# Given that we are including a subset of items that can be deleted while excluding the rest,
# subTest is appropriate here as otherwise we would need many duplicate tests for the same reason.
with less_console_noise():
draft_domain = DraftDomain.objects.create(name="igorville.gov")
for status in DomainApplication.ApplicationStatus:
if status not in [
DomainApplication.ApplicationStatus.STARTED,
DomainApplication.ApplicationStatus.WITHDRAWN,
]:
with self.subTest(status=status):
application = DomainApplication.objects.create(
creator=self.user, requested_domain=draft_domain, status=status
)
# Trigger the delete logic
response = self.client.post(
reverse("application-delete", kwargs={"pk": application.pk}), follow=True
)
# Check for a 403 error - the end user should not be allowed to do this
self.assertEqual(response.status_code, 403)
desired_application = DomainApplication.objects.filter(requested_domain=draft_domain)
# Make sure the DomainApplication wasn't deleted
self.assertEqual(desired_application.count(), 1)
# clean up
application.delete()
def test_home_deletes_domain_application_and_orphans(self):
"""Tests if delete for DomainApplication deletes orphaned Contact objects"""
# Create the site and contacts to delete (orphaned)
contact = Contact.objects.create(
first_name="Henry",
last_name="Mcfakerson",
)
contact_shared = Contact.objects.create(
first_name="Relative",
last_name="Aether",
)
# Create two non-orphaned contacts
contact_2 = Contact.objects.create(
first_name="Saturn",
last_name="Mars",
)
# Attach a user object to a contact (should not be deleted)
contact_user, _ = Contact.objects.get_or_create(user=self.user)
site = DraftDomain.objects.create(name="igorville.gov")
application = DomainApplication.objects.create(
creator=self.user,
requested_domain=site,
status=DomainApplication.ApplicationStatus.WITHDRAWN,
authorizing_official=contact,
submitter=contact_user,
)
application.other_contacts.set([contact_2])
# Create a second application to attach contacts to
site_2 = DraftDomain.objects.create(name="teaville.gov")
application_2 = DomainApplication.objects.create(
creator=self.user,
requested_domain=site_2,
status=DomainApplication.ApplicationStatus.STARTED,
authorizing_official=contact_2,
submitter=contact_shared,
)
application_2.other_contacts.set([contact_shared])
# Ensure that igorville.gov exists on the page
home_page = self.client.get("/")
self.assertContains(home_page, "igorville.gov")
# Trigger the delete logic
response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True)
# igorville is now deleted
self.assertNotContains(response, "igorville.gov")
# Check if the orphaned contact was deleted
orphan = Contact.objects.filter(id=contact.id)
self.assertFalse(orphan.exists())
# All non-orphan contacts should still exist and are unaltered
try:
current_user = Contact.objects.filter(id=contact_user.id).get()
except Contact.DoesNotExist:
self.fail("contact_user (a non-orphaned contact) was deleted")
self.assertEqual(current_user, contact_user)
try:
edge_case = Contact.objects.filter(id=contact_2.id).get()
except Contact.DoesNotExist:
self.fail("contact_2 (a non-orphaned contact) was deleted")
self.assertEqual(edge_case, contact_2)
def test_home_deletes_domain_application_and_shared_orphans(self):
"""Test the edge case for an object that will become orphaned after a delete
(but is not an orphan at the time of deletion)"""
# Create the site and contacts to delete (orphaned)
contact = Contact.objects.create(
first_name="Henry",
last_name="Mcfakerson",
)
contact_shared = Contact.objects.create(
first_name="Relative",
last_name="Aether",
)
# Create two non-orphaned contacts
contact_2 = Contact.objects.create(
first_name="Saturn",
last_name="Mars",
)
# Attach a user object to a contact (should not be deleted)
contact_user, _ = Contact.objects.get_or_create(user=self.user)
site = DraftDomain.objects.create(name="igorville.gov")
application = DomainApplication.objects.create(
creator=self.user,
requested_domain=site,
status=DomainApplication.ApplicationStatus.WITHDRAWN,
authorizing_official=contact,
submitter=contact_user,
)
application.other_contacts.set([contact_2])
# Create a second application to attach contacts to
site_2 = DraftDomain.objects.create(name="teaville.gov")
application_2 = DomainApplication.objects.create(
creator=self.user,
requested_domain=site_2,
status=DomainApplication.ApplicationStatus.STARTED,
authorizing_official=contact_2,
submitter=contact_shared,
)
application_2.other_contacts.set([contact_shared])
home_page = self.client.get("/")
self.assertContains(home_page, "teaville.gov")
# Trigger the delete logic
response = self.client.post(reverse("application-delete", kwargs={"pk": application_2.pk}), follow=True)
self.assertNotContains(response, "teaville.gov")
# Check if the orphaned contact was deleted
orphan = Contact.objects.filter(id=contact_shared.id)
self.assertFalse(orphan.exists())
def test_application_form_view(self):
response = self.client.get("/request/", follow=True)
self.assertContains(
response,
"Youre about to start your .gov domain request.",
)
def test_domain_application_form_with_ineligible_user(self):
"""Application form not accessible for an ineligible user.
This test should be solid enough since all application wizard
views share the same permissions class"""
self.user.status = User.RESTRICTED
self.user.save()
with less_console_noise():
response = self.client.get("/request/", follow=True)
self.assertEqual(response.status_code, 403)

View file

@ -218,6 +218,7 @@ class TestDomainDetail(TestDomainOverview):
It shows as 'DNS needed'""" It shows as 'DNS needed'"""
# At the time of this test's writing, there are 6 UNKNOWN domains inherited # At the time of this test's writing, there are 6 UNKNOWN domains inherited
# from constructors. Let's reset. # from constructors. Let's reset.
with less_console_noise():
Domain.objects.all().delete() Domain.objects.all().delete()
UserDomainRole.objects.all().delete() UserDomainRole.objects.all().delete()
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
@ -238,6 +239,7 @@ class TestDomainDetail(TestDomainOverview):
It shows as 'DNS needed'""" It shows as 'DNS needed'"""
# At the time of this test's writing, there are 6 UNKNOWN domains inherited # At the time of this test's writing, there are 6 UNKNOWN domains inherited
# from constructors. Let's reset. # from constructors. Let's reset.
with less_console_noise():
Domain.objects.all().delete() Domain.objects.all().delete()
UserDomainRole.objects.all().delete() UserDomainRole.objects.all().delete()
@ -260,16 +262,17 @@ class TestDomainDetail(TestDomainOverview):
"""We could easily duplicate this test for all domain management """We could easily duplicate this test for all domain management
views, but a single url test should be solid enough since all domain views, but a single url test should be solid enough since all domain
management pages share the same permissions class""" management pages share the same permissions class"""
with less_console_noise():
self.user.status = User.RESTRICTED self.user.status = User.RESTRICTED
self.user.save() self.user.save()
home_page = self.app.get("/") home_page = self.app.get("/")
self.assertContains(home_page, "igorville.gov") self.assertContains(home_page, "igorville.gov")
with less_console_noise():
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_domain_detail_allowed_for_on_hold(self): def test_domain_detail_allowed_for_on_hold(self):
"""Test that the domain overview page displays for on hold domain""" """Test that the domain overview page displays for on hold domain"""
with less_console_noise():
home_page = self.app.get("/") home_page = self.app.get("/")
self.assertContains(home_page, "on-hold.gov") self.assertContains(home_page, "on-hold.gov")
@ -278,6 +281,7 @@ class TestDomainDetail(TestDomainOverview):
self.assertNotContains(detail_page, "Edit") self.assertNotContains(detail_page, "Edit")
def test_domain_detail_see_just_nameserver(self): def test_domain_detail_see_just_nameserver(self):
with less_console_noise():
home_page = self.app.get("/") home_page = self.app.get("/")
self.assertContains(home_page, "justnameserver.com") self.assertContains(home_page, "justnameserver.com")
@ -289,6 +293,7 @@ class TestDomainDetail(TestDomainOverview):
self.assertContains(detail_page, "ns2.justnameserver.com") self.assertContains(detail_page, "ns2.justnameserver.com")
def test_domain_detail_see_nameserver_and_ip(self): def test_domain_detail_see_nameserver_and_ip(self):
with less_console_noise():
home_page = self.app.get("/") home_page = self.app.get("/")
self.assertContains(home_page, "nameserverwithip.gov") self.assertContains(home_page, "nameserverwithip.gov")
@ -307,6 +312,7 @@ class TestDomainDetail(TestDomainOverview):
def test_domain_detail_with_no_information_or_application(self): def test_domain_detail_with_no_information_or_application(self):
"""Test that domain management page returns 200 and displays error """Test that domain management page returns 200 and displays error
when no domain information or domain application exist""" when no domain information or domain application exist"""
with less_console_noise():
# have to use staff user for this test # have to use staff user for this test
staff_user = create_user() staff_user = create_user()
# staff_user.save() # staff_user.save()
@ -332,7 +338,6 @@ class TestDomainManagers(TestDomainOverview):
super().tearDown() super().tearDown()
self.user.is_staff = False self.user.is_staff = False
self.user.save() self.user.save()
User.objects.all().delete()
def test_domain_managers(self): def test_domain_managers(self):
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
@ -348,183 +353,6 @@ class TestDomainManagers(TestDomainOverview):
response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
self.assertContains(response, "Add a domain manager") self.assertContains(response, "Add a domain manager")
def test_domain_user_delete(self):
"""Tests if deleting a domain manager works"""
# Add additional users
dummy_user_1 = User.objects.create(
username="macncheese",
email="cheese@igorville.com",
)
dummy_user_2 = User.objects.create(
username="pastapizza",
email="pasta@igorville.com",
)
role_1 = UserDomainRole.objects.create(user=dummy_user_1, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
role_2 = UserDomainRole.objects.create(user=dummy_user_2, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
# Make sure we're on the right page
self.assertContains(response, "Domain managers")
# Make sure the desired user exists
self.assertContains(response, "cheese@igorville.com")
# Delete dummy_user_1
response = self.client.post(
reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": dummy_user_1.id}), follow=True
)
# Grab the displayed messages
messages = list(response.context["messages"])
self.assertEqual(len(messages), 1)
# Ensure the error we recieve is in line with what we expect
message = messages[0]
self.assertEqual(message.message, "Removed cheese@igorville.com as a manager for this domain.")
self.assertEqual(message.tags, "success")
# Check that role_1 deleted in the DB after the post
deleted_user_exists = UserDomainRole.objects.filter(id=role_1.id).exists()
self.assertFalse(deleted_user_exists)
# Ensure that the current user wasn't deleted
current_user_exists = UserDomainRole.objects.filter(user=self.user.id, domain=self.domain).exists()
self.assertTrue(current_user_exists)
# Ensure that the other userdomainrole was not deleted
role_2_exists = UserDomainRole.objects.filter(id=role_2.id).exists()
self.assertTrue(role_2_exists)
def test_domain_user_delete_denied_if_no_permission(self):
"""Deleting a domain manager is denied if the user has no permission to do so"""
# Create a domain object
vip_domain = Domain.objects.create(name="freeman.gov")
# Add users
dummy_user_1 = User.objects.create(
username="bagel",
email="bagel@igorville.com",
)
dummy_user_2 = User.objects.create(
username="pastapizza",
email="pasta@igorville.com",
)
role_1 = UserDomainRole.objects.create(user=dummy_user_1, domain=vip_domain, role=UserDomainRole.Roles.MANAGER)
role_2 = UserDomainRole.objects.create(user=dummy_user_2, domain=vip_domain, role=UserDomainRole.Roles.MANAGER)
response = self.client.get(reverse("domain-users", kwargs={"pk": vip_domain.id}))
# Make sure that we can't access the domain manager page normally
self.assertEqual(response.status_code, 403)
# Try to delete dummy_user_1
response = self.client.post(
reverse("domain-user-delete", kwargs={"pk": vip_domain.id, "user_pk": dummy_user_1.id}), follow=True
)
# Ensure that we are denied access
self.assertEqual(response.status_code, 403)
# Ensure that the user wasn't deleted
role_1_exists = UserDomainRole.objects.filter(id=role_1.id).exists()
self.assertTrue(role_1_exists)
# Ensure that the other userdomainrole was not deleted
role_2_exists = UserDomainRole.objects.filter(id=role_2.id).exists()
self.assertTrue(role_2_exists)
# Make sure that the current user wasn't deleted for some reason
current_user_exists = UserDomainRole.objects.filter(user=dummy_user_1.id, domain=vip_domain.id).exists()
self.assertTrue(current_user_exists)
def test_domain_user_delete_denied_if_last_man_standing(self):
"""Deleting a domain manager is denied if the user is the only manager"""
# Create a domain object
vip_domain = Domain.objects.create(name="olive-oil.gov")
# Add the requesting user as the only manager on the domain
UserDomainRole.objects.create(user=self.user, domain=vip_domain, role=UserDomainRole.Roles.MANAGER)
response = self.client.get(reverse("domain-users", kwargs={"pk": vip_domain.id}))
# Make sure that we can still access the domain manager page normally
self.assertContains(response, "Domain managers")
# Make sure that the logged in user exists
self.assertContains(response, "info@example.com")
# Try to delete the current user
response = self.client.post(
reverse("domain-user-delete", kwargs={"pk": vip_domain.id, "user_pk": self.user.id}), follow=True
)
# Ensure that we are denied access
self.assertEqual(response.status_code, 403)
# Make sure that the current user wasn't deleted
current_user_exists = UserDomainRole.objects.filter(user=self.user.id, domain=vip_domain.id).exists()
self.assertTrue(current_user_exists)
def test_domain_user_delete_self_redirects_home(self):
"""Tests if deleting yourself redirects to home"""
# Add additional users
dummy_user_1 = User.objects.create(
username="macncheese",
email="cheese@igorville.com",
)
dummy_user_2 = User.objects.create(
username="pastapizza",
email="pasta@igorville.com",
)
role_1 = UserDomainRole.objects.create(user=dummy_user_1, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
role_2 = UserDomainRole.objects.create(user=dummy_user_2, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
# Make sure we're on the right page
self.assertContains(response, "Domain managers")
# Make sure the desired user exists
self.assertContains(response, "info@example.com")
# Make sure more than one UserDomainRole exists on this object
self.assertContains(response, "cheese@igorville.com")
# Delete the current user
response = self.client.post(
reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": self.user.id}), follow=True
)
# Check if we've been redirected to the home page
self.assertContains(response, "Manage your domains")
# Grab the displayed messages
messages = list(response.context["messages"])
self.assertEqual(len(messages), 1)
# Ensure the error we recieve is in line with what we expect
message = messages[0]
self.assertEqual(message.message, "You are no longer managing the domain igorville.gov.")
self.assertEqual(message.tags, "success")
# Ensure that the current user was deleted
current_user_exists = UserDomainRole.objects.filter(user=self.user.id, domain=self.domain).exists()
self.assertFalse(current_user_exists)
# Ensure that the other userdomainroles are not deleted
role_1_exists = UserDomainRole.objects.filter(id=role_1.id).exists()
self.assertTrue(role_1_exists)
role_2_exists = UserDomainRole.objects.filter(id=role_2.id).exists()
self.assertTrue(role_2_exists)
@boto3_mocking.patching @boto3_mocking.patching
def test_domain_user_add_form(self): def test_domain_user_add_form(self):
"""Adding an existing user works.""" """Adding an existing user works."""
@ -1278,6 +1106,7 @@ class TestDomainContactInformation(TestDomainOverview):
class TestDomainSecurityEmail(TestDomainOverview): class TestDomainSecurityEmail(TestDomainOverview):
def test_domain_security_email_existing_security_contact(self): def test_domain_security_email_existing_security_contact(self):
"""Can load domain's security email page.""" """Can load domain's security email page."""
with less_console_noise():
self.mockSendPatch = patch("registrar.models.domain.registry.send") self.mockSendPatch = patch("registrar.models.domain.registry.send")
self.mockedSendFunction = self.mockSendPatch.start() self.mockedSendFunction = self.mockSendPatch.start()
self.mockedSendFunction.side_effect = self.mockSend self.mockedSendFunction.side_effect = self.mockSend
@ -1295,6 +1124,7 @@ class TestDomainSecurityEmail(TestDomainOverview):
def test_domain_security_email_no_security_contact(self): def test_domain_security_email_no_security_contact(self):
"""Loads a domain with no defined security email. """Loads a domain with no defined security email.
We should not show the default.""" We should not show the default."""
with less_console_noise():
self.mockSendPatch = patch("registrar.models.domain.registry.send") self.mockSendPatch = patch("registrar.models.domain.registry.send")
self.mockedSendFunction = self.mockSendPatch.start() self.mockedSendFunction = self.mockSendPatch.start()
self.mockedSendFunction.side_effect = self.mockSend self.mockedSendFunction.side_effect = self.mockSend
@ -1308,6 +1138,7 @@ class TestDomainSecurityEmail(TestDomainOverview):
def test_domain_security_email(self): def test_domain_security_email(self):
"""Can load domain's security email page.""" """Can load domain's security email page."""
with less_console_noise():
page = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) page = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain.id}))
self.assertContains(page, "Security email") self.assertContains(page, "Security email")
@ -1315,6 +1146,7 @@ class TestDomainSecurityEmail(TestDomainOverview):
"""Adding a security email works. """Adding a security email works.
Uses self.app WebTest because we need to interact with forms. Uses self.app WebTest because we need to interact with forms.
""" """
with less_console_noise():
security_email_page = self.app.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) security_email_page = self.app.get(reverse("domain-security-email", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
security_email_page.form["security_email"] = "mayor@igorville.gov" security_email_page.form["security_email"] = "mayor@igorville.gov"
@ -1333,25 +1165,22 @@ class TestDomainSecurityEmail(TestDomainOverview):
success_page = result.follow() success_page = result.follow()
self.assertContains(success_page, "The security email for this domain has been updated") self.assertContains(success_page, "The security email for this domain has been updated")
def test_security_email_form_messages(self): def test_domain_security_email_form_messages(self):
""" """
Test against the success and error messages that are defined in the view Test against the success and error messages that are defined in the view
""" """
with less_console_noise():
p = "adminpass" p = "adminpass"
self.client.login(username="superuser", password=p) self.client.login(username="superuser", password=p)
form_data_registry_error = { form_data_registry_error = {
"security_email": "test@failCreate.gov", "security_email": "test@failCreate.gov",
} }
form_data_contact_error = { form_data_contact_error = {
"security_email": "test@contactError.gov", "security_email": "test@contactError.gov",
} }
form_data_success = { form_data_success = {
"security_email": "test@something.gov", "security_email": "test@something.gov",
} }
test_cases = [ test_cases = [
( (
"RegistryError", "RegistryError",
@ -1370,17 +1199,14 @@ class TestDomainSecurityEmail(TestDomainOverview):
), ),
# Add more test cases with different scenarios here # Add more test cases with different scenarios here
] ]
for test_name, data, expected_message in test_cases: for test_name, data, expected_message in test_cases:
response = self.client.post( response = self.client.post(
reverse("domain-security-email", kwargs={"pk": self.domain.id}), reverse("domain-security-email", kwargs={"pk": self.domain.id}),
data=data, data=data,
follow=True, follow=True,
) )
# Check the response status code, content, or any other relevant assertions # Check the response status code, content, or any other relevant assertions
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# Check if the expected message tag is set # Check if the expected message tag is set
if test_name == "RegistryError" or test_name == "ContactError": if test_name == "RegistryError" or test_name == "ContactError":
message_tag = "error" message_tag = "error"
@ -1389,7 +1215,6 @@ class TestDomainSecurityEmail(TestDomainOverview):
else: else:
# Handle other cases if needed # Handle other cases if needed
message_tag = "info" # Change to the appropriate default message_tag = "info" # Change to the appropriate default
# Check the message tag # Check the message tag
messages = list(response.context["messages"]) messages = list(response.context["messages"])
self.assertEqual(len(messages), 1) self.assertEqual(len(messages), 1)
@ -1411,7 +1236,6 @@ class TestDomainSecurityEmail(TestDomainOverview):
class TestDomainDNSSEC(TestDomainOverview): class TestDomainDNSSEC(TestDomainOverview):
"""MockEPPLib is already inherited.""" """MockEPPLib is already inherited."""
def test_dnssec_page_refreshes_enable_button(self): def test_dnssec_page_refreshes_enable_button(self):

View file

@ -9,7 +9,7 @@ from django.db.models import F, Value, CharField
from django.db.models.functions import Concat, Coalesce from django.db.models.functions import Concat, Coalesce
from registrar.models.public_contact import PublicContact from registrar.models.public_contact import PublicContact
from registrar.utility.enums import DefaultEmail
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -17,8 +17,9 @@ logger = logging.getLogger(__name__)
def write_header(writer, columns): def write_header(writer, columns):
""" """
Receives params from the parent methods and outputs a CSV with a header row. Receives params from the parent methods and outputs a CSV with a header row.
Works with write_header as longas the same writer object is passed. Works with write_header as long as the same writer object is passed.
""" """
writer.writerow(columns) writer.writerow(columns)
@ -43,7 +44,7 @@ def get_domain_infos(filter_condition, sort_fields):
return domain_infos_cleaned return domain_infos_cleaned
def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None): def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None, get_domain_managers=False):
"""Given a set of columns, generate a new row from cleaned column data""" """Given a set of columns, generate a new row from cleaned column data"""
# Domain should never be none when parsing this information # Domain should never be none when parsing this information
@ -65,7 +66,7 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None
security_email = _email if _email is not None else " " security_email = _email if _email is not None else " "
# These are default emails that should not be displayed in the csv report # These are default emails that should not be displayed in the csv report
invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"} invalid_emails = {DefaultEmail.LEGACY_DEFAULT.value, DefaultEmail.PUBLIC_CONTACT_DEFAULT.value}
if security_email.lower() in invalid_emails: if security_email.lower() in invalid_emails:
security_email = "(blank)" security_email = "(blank)"
@ -77,6 +78,8 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None
# create a dictionary of fields which can be included in output # create a dictionary of fields which can be included in output
FIELDS = { FIELDS = {
"Domain name": domain.name, "Domain name": domain.name,
"Status": domain.get_state_display(),
"Expiration date": domain.expiration_date,
"Domain type": domain_type, "Domain type": domain_type,
"Agency": domain_info.federal_agency, "Agency": domain_info.federal_agency,
"Organization name": domain_info.organization_name, "Organization name": domain_info.organization_name,
@ -85,33 +88,27 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None
"AO": domain_info.ao, # type: ignore "AO": domain_info.ao, # type: ignore
"AO email": domain_info.authorizing_official.email if domain_info.authorizing_official else " ", "AO email": domain_info.authorizing_official.email if domain_info.authorizing_official else " ",
"Security contact email": security_email, "Security contact email": security_email,
"Status": domain.get_state_display(),
"Expiration date": domain.expiration_date,
"Created at": domain.created_at, "Created at": domain.created_at,
"First ready": domain.first_ready, "First ready": domain.first_ready,
"Deleted": domain.deleted, "Deleted": domain.deleted,
} }
if get_domain_managers:
# Get each domain managers email and add to list
dm_emails = [dm.user.email for dm in domain.permissions.all()]
# Set up the "matching header" + row field data
for i, dm_email in enumerate(dm_emails, start=1):
FIELDS[f"Domain manager email {i}"] = dm_email
row = [FIELDS.get(column, "") for column in columns] row = [FIELDS.get(column, "") for column in columns]
return row return row
def write_body( def _get_security_emails(sec_contact_ids):
writer,
columns,
sort_fields,
filter_condition,
):
""" """
Receives params from the parent methods and outputs a CSV with fltered and sorted domains. Retrieve security contact emails for the given security contact IDs.
Works with write_header as longas the same writer object is passed.
""" """
# Get the domainInfos
all_domain_infos = get_domain_infos(filter_condition, sort_fields)
# Store all security emails to avoid epp calls or excessive filters
sec_contact_ids = all_domain_infos.values_list("domain__security_contact_registry_id", flat=True)
security_emails_dict = {} security_emails_dict = {}
public_contacts = ( public_contacts = (
PublicContact.objects.only("email", "domain__name") PublicContact.objects.only("email", "domain__name")
@ -127,14 +124,55 @@ def write_body(
else: else:
logger.warning("csv_export -> Domain was none for PublicContact") logger.warning("csv_export -> Domain was none for PublicContact")
return security_emails_dict
def update_columns_with_domain_managers(columns, max_dm_count):
"""
Update the columns list to include "Domain manager email {#}" headers
based on the maximum domain manager count.
"""
for i in range(1, max_dm_count + 1):
columns.append(f"Domain manager email {i}")
def write_csv(
writer,
columns,
sort_fields,
filter_condition,
get_domain_managers=False,
should_write_header=True,
):
"""
Receives params from the parent methods and outputs a CSV with fltered and sorted domains.
Works with write_header as longas the same writer object is passed.
get_domain_managers: Conditional bc we only use domain manager info for export_data_full_to_csv
should_write_header: Conditional bc export_data_growth_to_csv calls write_body twice
"""
all_domain_infos = get_domain_infos(filter_condition, sort_fields)
# Store all security emails to avoid epp calls or excessive filters
sec_contact_ids = all_domain_infos.values_list("domain__security_contact_registry_id", flat=True)
security_emails_dict = _get_security_emails(sec_contact_ids)
# Reduce the memory overhead when performing the write operation # Reduce the memory overhead when performing the write operation
paginator = Paginator(all_domain_infos, 1000) paginator = Paginator(all_domain_infos, 1000)
if get_domain_managers and len(all_domain_infos) > 0:
# We want to get the max amont of domain managers an
# account has to set the column header dynamically
max_dm_count = max(len(domain_info.domain.permissions.all()) for domain_info in all_domain_infos)
update_columns_with_domain_managers(columns, max_dm_count)
for page_num in paginator.page_range: for page_num in paginator.page_range:
page = paginator.page(page_num) page = paginator.page(page_num)
rows = [] rows = []
for domain_info in page.object_list: for domain_info in page.object_list:
try: try:
row = parse_row(columns, domain_info, security_emails_dict) row = parse_row(columns, domain_info, security_emails_dict, get_domain_managers)
rows.append(row) rows.append(row)
except ValueError: except ValueError:
# This should not happen. If it does, just skip this row. # This should not happen. If it does, just skip this row.
@ -142,6 +180,9 @@ def write_body(
logger.error("csv_export -> Error when parsing row, domain was None") logger.error("csv_export -> Error when parsing row, domain was None")
continue continue
if should_write_header:
write_header(writer, columns)
writer.writerows(rows) writer.writerows(rows)
@ -152,6 +193,8 @@ def export_data_type_to_csv(csv_file):
# define columns to include in export # define columns to include in export
columns = [ columns = [
"Domain name", "Domain name",
"Status",
"Expiration date",
"Domain type", "Domain type",
"Agency", "Agency",
"Organization name", "Organization name",
@ -160,9 +203,9 @@ def export_data_type_to_csv(csv_file):
"AO", "AO",
"AO email", "AO email",
"Security contact email", "Security contact email",
"Status", # For domain manager we are pass it in as a parameter below in write_body
"Expiration date",
] ]
# Coalesce is used to replace federal_type of None with ZZZZZ # Coalesce is used to replace federal_type of None with ZZZZZ
sort_fields = [ sort_fields = [
"organization_type", "organization_type",
@ -177,8 +220,7 @@ def export_data_type_to_csv(csv_file):
Domain.State.ON_HOLD, Domain.State.ON_HOLD,
], ],
} }
write_header(writer, columns) write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=True, should_write_header=True)
write_body(writer, columns, sort_fields, filter_condition)
def export_data_full_to_csv(csv_file): def export_data_full_to_csv(csv_file):
@ -209,8 +251,7 @@ def export_data_full_to_csv(csv_file):
Domain.State.ON_HOLD, Domain.State.ON_HOLD,
], ],
} }
write_header(writer, columns) write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True)
write_body(writer, columns, sort_fields, filter_condition)
def export_data_federal_to_csv(csv_file): def export_data_federal_to_csv(csv_file):
@ -242,8 +283,7 @@ def export_data_federal_to_csv(csv_file):
Domain.State.ON_HOLD, Domain.State.ON_HOLD,
], ],
} }
write_header(writer, columns) write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True)
write_body(writer, columns, sort_fields, filter_condition)
def get_default_start_date(): def get_default_start_date():
@ -310,6 +350,12 @@ def export_data_growth_to_csv(csv_file, start_date, end_date):
"domain__deleted__gte": start_date_formatted, "domain__deleted__gte": start_date_formatted,
} }
write_header(writer, columns) write_csv(writer, columns, sort_fields, filter_condition, get_domain_managers=False, should_write_header=True)
write_body(writer, columns, sort_fields, filter_condition) write_csv(
write_body(writer, columns, sort_fields_for_deleted_domains, filter_condition_for_deleted_domains) writer,
columns,
sort_fields_for_deleted_domains,
filter_condition_for_deleted_domains,
get_domain_managers=False,
should_write_header=False,
)

View file

@ -10,7 +10,6 @@ logger = logging.getLogger(__name__)
class EmailSendingError(RuntimeError): class EmailSendingError(RuntimeError):
"""Local error for handling all failures when sending email.""" """Local error for handling all failures when sending email."""
pass pass

View file

@ -26,3 +26,15 @@ class LogCode(Enum):
INFO = 3 INFO = 3
DEBUG = 4 DEBUG = 4
DEFAULT = 5 DEFAULT = 5
class DefaultEmail(Enum):
"""Stores the string values of default emails
Overview of emails:
- PUBLIC_CONTACT_DEFAULT: "dotgov@cisa.dhs.gov"
- LEGACY_DEFAULT: "registrar@dotgov.gov"
"""
PUBLIC_CONTACT_DEFAULT = "dotgov@cisa.dhs.gov"
LEGACY_DEFAULT = "registrar@dotgov.gov"

View file

@ -159,6 +159,10 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
def storage(self): def storage(self):
# marking session as modified on every access # marking session as modified on every access
# so that updates to nested keys are always saved # so that updates to nested keys are always saved
# Also - check that self.request.session has the attr
# modified to account for test environments calling
# view methods
if hasattr(self.request.session, "modified"):
self.request.session.modified = True self.request.session.modified = True
return self.request.session.setdefault(self.prefix, {}) return self.request.session.setdefault(self.prefix, {})
@ -211,6 +215,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
if current_url == self.EDIT_URL_NAME and "id" in kwargs: if current_url == self.EDIT_URL_NAME and "id" in kwargs:
del self.storage del self.storage
self.storage["application_id"] = kwargs["id"] self.storage["application_id"] = kwargs["id"]
self.storage["step_history"] = self.db_check_for_unlocking_steps()
# if accessing this class directly, redirect to the first step # if accessing this class directly, redirect to the first step
# in other words, if `ApplicationWizard` is called as view # in other words, if `ApplicationWizard` is called as view
@ -269,6 +274,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
and from the database if `use_db` is True (provided that record exists). and from the database if `use_db` is True (provided that record exists).
An empty form will be provided if neither of those are true. An empty form will be provided if neither of those are true.
""" """
kwargs = { kwargs = {
"files": files, "files": files,
"prefix": self.steps.current, "prefix": self.steps.current,
@ -329,6 +335,43 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
] ]
return DomainApplication.objects.filter(creator=self.request.user, status__in=check_statuses) return DomainApplication.objects.filter(creator=self.request.user, status__in=check_statuses)
def db_check_for_unlocking_steps(self):
"""Helper for get_context_data
Queries the DB for an application and returns a list of unlocked steps."""
history_dict = {
"organization_type": self.application.organization_type is not None,
"tribal_government": self.application.tribe_name is not None,
"organization_federal": self.application.federal_type is not None,
"organization_election": self.application.is_election_board is not None,
"organization_contact": (
self.application.federal_agency is not None
or self.application.organization_name is not None
or self.application.address_line1 is not None
or self.application.city is not None
or self.application.state_territory is not None
or self.application.zipcode is not None
or self.application.urbanization is not None
),
"about_your_organization": self.application.about_your_organization is not None,
"authorizing_official": self.application.authorizing_official is not None,
"current_sites": (
self.application.current_websites.exists() or self.application.requested_domain is not None
),
"dotgov_domain": self.application.requested_domain is not None,
"purpose": self.application.purpose is not None,
"your_contact": self.application.submitter is not None,
"other_contacts": (
self.application.other_contacts.exists() or self.application.no_other_contacts_rationale is not None
),
"anything_else": (
self.application.anything_else is not None or self.application.is_policy_acknowledged is not None
),
"requirements": self.application.is_policy_acknowledged is not None,
"review": self.application.is_policy_acknowledged is not None,
}
return [key for key, value in history_dict.items() if value]
def get_context_data(self): def get_context_data(self):
"""Define context for access on all wizard pages.""" """Define context for access on all wizard pages."""
# Build the submit button that we'll pass to the modal. # Build the submit button that we'll pass to the modal.
@ -338,6 +381,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
modal_heading = "You are about to submit a domain request for " + str(self.application.requested_domain) modal_heading = "You are about to submit a domain request for " + str(self.application.requested_domain)
else: else:
modal_heading = "You are about to submit an incomplete request" modal_heading = "You are about to submit an incomplete request"
return { return {
"form_titles": self.TITLES, "form_titles": self.TITLES,
"steps": self.steps, "steps": self.steps,

View file

@ -22,6 +22,7 @@ from registrar.models import (
UserDomainRole, UserDomainRole,
) )
from registrar.models.public_contact import PublicContact from registrar.models.public_contact import PublicContact
from registrar.utility.enums import DefaultEmail
from registrar.utility.errors import ( from registrar.utility.errors import (
GenericError, GenericError,
GenericErrorCodes, GenericErrorCodes,
@ -134,7 +135,6 @@ class DomainFormBaseView(DomainBaseView, FormMixin):
class DomainView(DomainBaseView): class DomainView(DomainBaseView):
"""Domain detail overview page.""" """Domain detail overview page."""
template_name = "domain_detail.html" template_name = "domain_detail.html"
@ -142,11 +142,12 @@ class DomainView(DomainBaseView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
default_email = self.object.get_default_security_contact().email default_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value]
context["default_security_email"] = default_email
context["hidden_security_emails"] = default_emails
security_email = self.object.get_security_email() security_email = self.object.get_security_email()
if security_email is None or security_email == default_email: if security_email is None or security_email in default_emails:
context["security_email"] = None context["security_email"] = None
return context return context
context["security_email"] = security_email context["security_email"] = security_email
@ -553,7 +554,7 @@ class DomainYourContactInformationView(DomainFormBaseView):
# Post to DB using values from the form # Post to DB using values from the form
form.save() form.save()
messages.success(self.request, "Your contact information has been updated.") messages.success(self.request, "Your contact information for all your domains has been updated.")
# superclass has the redirect # superclass has the redirect
return super().form_valid(form) return super().form_valid(form)
@ -570,7 +571,7 @@ class DomainSecurityEmailView(DomainFormBaseView):
initial = super().get_initial() initial = super().get_initial()
security_contact = self.object.security_contact security_contact = self.object.security_contact
invalid_emails = ["dotgov@cisa.dhs.gov", "registrar@dotgov.gov"] invalid_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value]
if security_contact is None or security_contact.email in invalid_emails: if security_contact is None or security_contact.email in invalid_emails:
initial["security_email"] = None initial["security_email"] = None
return initial return initial
@ -785,14 +786,17 @@ class DomainAddUserView(DomainFormBaseView):
return redirect(self.get_success_url()) return redirect(self.get_success_url())
class DomainInvitationDeleteView(DomainInvitationPermissionDeleteView, SuccessMessageMixin): # The order of the superclasses matters here. BaseDeleteView has a bug where the
# "form_valid" function does not call super, so it cannot use SuccessMessageMixin.
# The workaround is to use SuccessMessageMixin first.
class DomainInvitationDeleteView(SuccessMessageMixin, DomainInvitationPermissionDeleteView):
object: DomainInvitation # workaround for type mismatch in DeleteView object: DomainInvitation # workaround for type mismatch in DeleteView
def get_success_url(self): def get_success_url(self):
return reverse("domain-users", kwargs={"pk": self.object.domain.id}) return reverse("domain-users", kwargs={"pk": self.object.domain.id})
def get_success_message(self, cleaned_data): def get_success_message(self, cleaned_data):
return f"Successfully canceled invitation for {self.object.email}." return f"Canceled invitation to {self.object.email}."
class DomainDeleteUserView(UserDomainRolePermissionDeleteView): class DomainDeleteUserView(UserDomainRolePermissionDeleteView):

View file

@ -146,7 +146,6 @@ class OrderableFieldsMixin:
class PermissionsLoginMixin(PermissionRequiredMixin): class PermissionsLoginMixin(PermissionRequiredMixin):
"""Mixin that redirects to login page if not logged in, otherwise 403.""" """Mixin that redirects to login page if not logged in, otherwise 403."""
def handle_no_permission(self): def handle_no_permission(self):
@ -155,7 +154,6 @@ class PermissionsLoginMixin(PermissionRequiredMixin):
class DomainPermission(PermissionsLoginMixin): class DomainPermission(PermissionsLoginMixin):
"""Permission mixin that redirects to domain if user has access, """Permission mixin that redirects to domain if user has access,
otherwise 403""" otherwise 403"""
@ -264,7 +262,6 @@ class DomainPermission(PermissionsLoginMixin):
class DomainApplicationPermission(PermissionsLoginMixin): class DomainApplicationPermission(PermissionsLoginMixin):
"""Permission mixin that redirects to domain application if user """Permission mixin that redirects to domain application if user
has access, otherwise 403""" has access, otherwise 403"""
@ -287,7 +284,6 @@ class DomainApplicationPermission(PermissionsLoginMixin):
class UserDeleteDomainRolePermission(PermissionsLoginMixin): class UserDeleteDomainRolePermission(PermissionsLoginMixin):
"""Permission mixin for UserDomainRole if user """Permission mixin for UserDomainRole if user
has access, otherwise 403""" has access, otherwise 403"""
@ -324,7 +320,6 @@ class UserDeleteDomainRolePermission(PermissionsLoginMixin):
class DomainApplicationPermissionWithdraw(PermissionsLoginMixin): class DomainApplicationPermissionWithdraw(PermissionsLoginMixin):
"""Permission mixin that redirects to withdraw action on domain application """Permission mixin that redirects to withdraw action on domain application
if user has access, otherwise 403""" if user has access, otherwise 403"""
@ -347,7 +342,6 @@ class DomainApplicationPermissionWithdraw(PermissionsLoginMixin):
class ApplicationWizardPermission(PermissionsLoginMixin): class ApplicationWizardPermission(PermissionsLoginMixin):
"""Permission mixin that redirects to start or edit domain application if """Permission mixin that redirects to start or edit domain application if
user has access, otherwise 403""" user has access, otherwise 403"""
@ -365,7 +359,6 @@ class ApplicationWizardPermission(PermissionsLoginMixin):
class DomainInvitationPermission(PermissionsLoginMixin): class DomainInvitationPermission(PermissionsLoginMixin):
"""Permission mixin that redirects to domain invitation if user has """Permission mixin that redirects to domain invitation if user has
access, otherwise 403" access, otherwise 403"

View file

@ -20,7 +20,6 @@ logger = logging.getLogger(__name__)
class DomainPermissionView(DomainPermission, DetailView, abc.ABC): class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
"""Abstract base view for domains that enforces permissions. """Abstract base view for domains that enforces permissions.
This abstract view cannot be instantiated. Actual views must specify This abstract view cannot be instantiated. Actual views must specify
@ -58,7 +57,6 @@ class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, abc.ABC): class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, abc.ABC):
"""Abstract base view for domain applications that enforces permissions """Abstract base view for domain applications that enforces permissions
This abstract view cannot be instantiated. Actual views must specify This abstract view cannot be instantiated. Actual views must specify
@ -78,7 +76,6 @@ class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, a
class DomainApplicationPermissionWithdrawView(DomainApplicationPermissionWithdraw, DetailView, abc.ABC): class DomainApplicationPermissionWithdrawView(DomainApplicationPermissionWithdraw, DetailView, abc.ABC):
"""Abstract base view for domain application withdraw function """Abstract base view for domain application withdraw function
This abstract view cannot be instantiated. Actual views must specify This abstract view cannot be instantiated. Actual views must specify
@ -98,7 +95,6 @@ class DomainApplicationPermissionWithdrawView(DomainApplicationPermissionWithdra
class ApplicationWizardPermissionView(ApplicationWizardPermission, TemplateView, abc.ABC): class ApplicationWizardPermissionView(ApplicationWizardPermission, TemplateView, abc.ABC):
"""Abstract base view for the application form that enforces permissions """Abstract base view for the application form that enforces permissions
This abstract view cannot be instantiated. Actual views must specify This abstract view cannot be instantiated. Actual views must specify
@ -113,7 +109,6 @@ class ApplicationWizardPermissionView(ApplicationWizardPermission, TemplateView,
class DomainInvitationPermissionDeleteView(DomainInvitationPermission, DeleteView, abc.ABC): class DomainInvitationPermissionDeleteView(DomainInvitationPermission, DeleteView, abc.ABC):
"""Abstract view for deleting a domain invitation. """Abstract view for deleting a domain invitation.
This one is fairly specialized, but this is the only thing that we do This one is fairly specialized, but this is the only thing that we do
@ -127,7 +122,6 @@ class DomainInvitationPermissionDeleteView(DomainInvitationPermission, DeleteVie
class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteView, abc.ABC): class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteView, abc.ABC):
"""Abstract view for deleting a DomainApplication.""" """Abstract view for deleting a DomainApplication."""
model = DomainApplication model = DomainApplication
@ -135,7 +129,6 @@ class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteV
class UserDomainRolePermissionDeleteView(UserDeleteDomainRolePermission, DeleteView, abc.ABC): class UserDomainRolePermissionDeleteView(UserDeleteDomainRolePermission, DeleteView, abc.ABC):
"""Abstract base view for deleting a UserDomainRole. """Abstract base view for deleting a UserDomainRole.
This abstract view cannot be instantiated. Actual views must specify This abstract view cannot be instantiated. Actual views must specify

View file

@ -1,18 +1,18 @@
-i https://pypi.python.org/simple -i https://pypi.python.org/simple
annotated-types==0.6.0; python_version >= '3.8' annotated-types==0.6.0; python_version >= '3.8'
asgiref==3.7.2; python_version >= '3.7' asgiref==3.7.2; python_version >= '3.7'
boto3==1.33.7; python_version >= '3.7' boto3==1.34.37; python_version >= '3.8'
botocore==1.33.7; python_version >= '3.7' botocore==1.34.37; python_version >= '3.8'
cachetools==5.3.2; python_version >= '3.7' cachetools==5.3.2; python_version >= '3.7'
certifi==2023.11.17; python_version >= '3.6' certifi==2024.2.2; python_version >= '3.6'
cfenv==0.5.3 cfenv==0.5.3
cffi==1.16.0; python_version >= '3.8' cffi==1.16.0; platform_python_implementation != 'PyPy'
charset-normalizer==3.3.2; python_full_version >= '3.7.0' charset-normalizer==3.3.2; python_full_version >= '3.7.0'
cryptography==41.0.7; python_version >= '3.7' cryptography==42.0.2; python_version >= '3.7'
defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' defusedxml==0.7.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
dj-database-url==2.1.0 dj-database-url==2.1.0
dj-email-url==1.0.6 dj-email-url==1.0.6
django==4.2.7; python_version >= '3.8' django==4.2.10; python_version >= '3.8'
django-allow-cidr==0.7.1 django-allow-cidr==0.7.1
django-auditlog==2.3.0; python_version >= '3.7' django-auditlog==2.3.0; python_version >= '3.7'
django-cache-url==3.4.5 django-cache-url==3.4.5
@ -20,42 +20,42 @@ django-cors-headers==4.3.1; python_version >= '3.8'
django-csp==3.7 django-csp==3.7
django-fsm==2.8.1 django-fsm==2.8.1
django-login-required-middleware==0.9.0 django-login-required-middleware==0.9.0
django-phonenumber-field[phonenumberslite]==7.2.0; python_version >= '3.8' django-phonenumber-field[phonenumberslite]==7.3.0; python_version >= '3.8'
django-widget-tweaks==1.5.0; python_version >= '3.8' django-widget-tweaks==1.5.0; python_version >= '3.8'
environs[django]==9.5.0; python_version >= '3.6' environs[django]==10.3.0; python_version >= '3.8'
faker==20.1.0; python_version >= '3.8' faker==23.1.0; python_version >= '3.8'
fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c
furl==2.1.3 furl==2.1.3
future==0.18.3; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3' future==0.18.3; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
gevent==23.9.1; python_version >= '3.8' gevent==23.9.1; python_version >= '3.8'
geventconnpool@ git+https://github.com/rasky/geventconnpool.git@1bbb93a714a331a069adf27265fe582d9ba7ecd4 geventconnpool@ git+https://github.com/rasky/geventconnpool.git@1bbb93a714a331a069adf27265fe582d9ba7ecd4
greenlet==3.0.1; python_version >= '3.7' greenlet==3.0.3; python_version >= '3.7'
gunicorn==21.2.0; python_version >= '3.5' gunicorn==21.2.0; python_version >= '3.5'
idna==3.6; python_version >= '3.5' idna==3.6; python_version >= '3.5'
jmespath==1.0.1; python_version >= '3.7' jmespath==1.0.1; python_version >= '3.7'
lxml==4.9.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' lxml==5.1.0; python_version >= '3.6'
mako==1.3.0; python_version >= '3.8' mako==1.3.2; python_version >= '3.8'
markupsafe==2.1.3; python_version >= '3.7' markupsafe==2.1.5; python_version >= '3.7'
marshmallow==3.20.1; python_version >= '3.8' marshmallow==3.20.2; python_version >= '3.8'
oic==1.6.1; python_version ~= '3.7' oic==1.6.1; python_version ~= '3.7'
orderedmultidict==1.0.1 orderedmultidict==1.0.1
packaging==23.2; python_version >= '3.7' packaging==23.2; python_version >= '3.7'
phonenumberslite==8.13.26 phonenumberslite==8.13.29
psycopg2-binary==2.9.9; python_version >= '3.7' psycopg2-binary==2.9.9; python_version >= '3.7'
pycparser==2.21 pycparser==2.21
pycryptodomex==3.19.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' pycryptodomex==3.20.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
pydantic==2.5.2; python_version >= '3.7' pydantic==2.6.1; python_version >= '3.8'
pydantic-core==2.14.5; python_version >= '3.7' pydantic-core==2.16.2; python_version >= '3.8'
pydantic-settings==2.1.0; python_version >= '3.8' pydantic-settings==2.1.0; python_version >= '3.8'
pyjwkest==1.4.2 pyjwkest==1.4.2
python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
python-dotenv==1.0.0; python_version >= '3.8' python-dotenv==1.0.1; python_version >= '3.8'
requests==2.31.0; python_version >= '3.7' requests==2.31.0; python_version >= '3.7'
s3transfer==0.8.2; python_version >= '3.7' s3transfer==0.10.0; python_version >= '3.8'
setuptools==69.0.2; python_version >= '3.8' setuptools==69.0.3; python_version >= '3.8'
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
sqlparse==0.4.4; python_version >= '3.5' sqlparse==0.4.4; python_version >= '3.5'
typing-extensions==4.8.0; python_version >= '3.8' typing-extensions==4.9.0; python_version >= '3.8'
urllib3==2.0.7; python_version >= '3.7' urllib3==2.0.7; python_version >= '3.7'
whitenoise==6.6.0; python_version >= '3.8' whitenoise==6.6.0; python_version >= '3.8'
zope.event==5.0; python_version >= '3.7' zope.event==5.0; python_version >= '3.7'

View file

@ -6,4 +6,4 @@ set -o pipefail
# Make sure that django's `collectstatic` has been run locally before pushing up to any environment, # Make sure that django's `collectstatic` has been run locally before pushing up to any environment,
# so that the styles and static assets to show up correctly on any environment. # so that the styles and static assets to show up correctly on any environment.
gunicorn registrar.config.wsgi -t 60 gunicorn --worker-class=gevent registrar.config.wsgi -t 60