Fix conflict

This commit is contained in:
Rebecca Hsieh 2024-02-06 13:20:59 -08:00
commit 9a44a40a49
No known key found for this signature in database
42 changed files with 7114 additions and 6661 deletions

View file

@ -36,7 +36,7 @@ jobs:
- name: Unit tests - name: Unit tests
working-directory: ./src working-directory: ./src
run: docker compose run app python manage.py test run: docker compose run app python manage.py test --parallel
django-migrations-complete: django-migrations-complete:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View file

@ -145,7 +145,7 @@ You can change the logging verbosity, if needed. Do a web search for "django log
## Mock data ## Mock data
There is a `post_migrate` signal in [signals.py](../../src/registrar/signals.py) that will load the fixtures from [fixtures_user.py](../../src/registrar/fixtures_users.py) and [fixtures_applications.py](../../src/registrar/fixtures_applications.py), giving you some test data to play with while developing. [load.py](../../src/registrar/management/commands/load.py) called from docker-compose (locally) and reset-db.yml (upper) loads the fixtures from [fixtures_user.py](../../src/registrar/fixtures_users.py) and [fixtures_applications.py](../../src/registrar/fixtures_applications.py), giving you some test data to play with while developing.
See the [database-access README](./database-access.md) for information on how to pull data to update these fixtures. See the [database-access README](./database-access.md) for information on how to pull data to update these fixtures.

View file

@ -35,155 +35,155 @@ class ViewsTest(TestCase):
pass pass
def test_openid_sets_next(self, mock_client): def test_openid_sets_next(self, mock_client):
# setup with less_console_noise():
callback_url = reverse("openid_login_callback") # setup
# mock callback_url = reverse("openid_login_callback")
mock_client.create_authn_request.side_effect = self.say_hi # mock
mock_client.get_default_acr_value.side_effect = self.create_acr mock_client.create_authn_request.side_effect = self.say_hi
# test mock_client.get_default_acr_value.side_effect = self.create_acr
response = self.client.get(reverse("login"), {"next": callback_url}) # test
# assert response = self.client.get(reverse("login"), {"next": callback_url})
session = mock_client.create_authn_request.call_args[0][0] # assert
self.assertEqual(session["next"], callback_url) session = mock_client.create_authn_request.call_args[0][0]
self.assertEqual(response.status_code, 200) self.assertEqual(session["next"], callback_url)
self.assertContains(response, "Hi") self.assertEqual(response.status_code, 200)
self.assertContains(response, "Hi")
def test_openid_raises(self, mock_client): def test_openid_raises(self, mock_client):
# mock
mock_client.create_authn_request.side_effect = Exception("Test")
# test
with less_console_noise(): with less_console_noise():
# mock
mock_client.create_authn_request.side_effect = Exception("Test")
# test
response = self.client.get(reverse("login")) response = self.client.get(reverse("login"))
# assert # assert
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_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
mock_client.get_default_acr_value.side_effect = self.create_acr
mock_client.callback.side_effect = NoStateDefined()
# test
with less_console_noise(): with less_console_noise():
# mock
mock_client.get_default_acr_value.side_effect = self.create_acr
mock_client.callback.side_effect = NoStateDefined()
# test
response = self.client.get(reverse("openid_login_callback")) response = self.client.get(reverse("openid_login_callback"))
# assert # assert
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 with less_console_noise():
session = self.client.session # setup
session["next"] = reverse("logout") session = self.client.session
session.save() session["next"] = reverse("logout")
# mock session.save()
mock_client.callback.side_effect = self.user_info # mock
# test mock_client.callback.side_effect = self.user_info
with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): # test
response = self.client.get(reverse("openid_login_callback")) with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise():
# assert response = self.client.get(reverse("openid_login_callback"))
self.assertEqual(response.status_code, 302) # assert
self.assertEqual(response.url, reverse("logout")) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("logout"))
def test_login_callback_no_step_up_auth(self, mock_client): def test_login_callback_no_step_up_auth(self, mock_client):
"""Walk through login_callback when requires_step_up_auth returns False """Walk through login_callback when requires_step_up_auth returns False
and assert that we have a redirect to /""" and assert that we have a redirect to /"""
# setup with less_console_noise():
session = self.client.session # setup
session.save() session = self.client.session
# mock session.save()
mock_client.callback.side_effect = self.user_info # mock
# test mock_client.callback.side_effect = self.user_info
with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): # test
response = self.client.get(reverse("openid_login_callback")) with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise():
# assert response = self.client.get(reverse("openid_login_callback"))
self.assertEqual(response.status_code, 302) # assert
self.assertEqual(response.url, "/") self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/")
def test_requires_step_up_auth(self, mock_client): def test_requires_step_up_auth(self, mock_client):
"""Invoke login_callback passing it a request when requires_step_up_auth returns True """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."""
# Configure the mock to return an expected value for get_step_up_acr_value with less_console_noise():
mock_client.return_value.get_step_up_acr_value.return_value = "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"
# 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: login_callback(request)
login_callback(request) # create_authn_request only gets called when requires_step_up_auth is True
# and it changes this acr_value in request.session
# create_authn_request only gets called when requires_step_up_auth is True # Assert that acr_value is no longer empty string
# and it changes this acr_value in request.session self.assertNotEqual(request.session["acr_value"], "")
# And create_authn_request was called again
# Assert that acr_value is no longer empty string mock_create_authn_request.assert_called_once()
self.assertNotEqual(request.session["acr_value"], "")
# And create_authn_request was called again
mock_create_authn_request.assert_called_once()
def test_does_not_requires_step_up_auth(self, mock_client): def test_does_not_requires_step_up_auth(self, mock_client):
"""Invoke login_callback passing it a request when requires_step_up_auth returns False """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"""
# Create a mock request with less_console_noise():
request = self.factory.get("/some-url") # Create a mock request
request.session = {"acr_value": ""} request = self.factory.get("/some-url")
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:
login_callback(request) login_callback(request)
# 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
self.assertEqual(request.session["acr_value"], "")
# Assert that acr_value is NOT updated by testing that it is still an empty string # Assert create_authn_request was not called
self.assertEqual(request.session["acr_value"], "") mock_create_authn_request.assert_not_called()
# Assert create_authn_request was not called
mock_create_authn_request.assert_not_called()
@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 with less_console_noise():
mock_client.callback.side_effect = self.user_info # mock
mock_auth.return_value = None mock_client.callback.side_effect = self.user_info
# test mock_auth.return_value = None
with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): # test
response = self.client.get(reverse("openid_login_callback")) with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise():
# assert response = self.client.get(reverse("openid_login_callback"))
self.assertEqual(response.status_code, 401) # assert
self.assertTemplateUsed(response, "401.html") self.assertEqual(response.status_code, 401)
self.assertIn("Unauthorized", response.content.decode("utf-8")) self.assertTemplateUsed(response, "401.html")
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
session = self.client.session
session["state"] = "TEST" # nosec B105
session.save()
# 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(): with less_console_noise():
response = self.client.get(reverse("logout")) # setup
# assert session = self.client.session
expected = ( session["state"] = "TEST" # nosec B105
"http://example.com/log_me_out?client_id=TEST&state" session.save()
"=TEST&post_logout_redirect_uri=http%3A%2F%2Fexample.com%2Fback" # mock
) mock_client.callback.side_effect = self.user_info
actual = response.url mock_client.registration_response = {"post_logout_redirect_uris": ["http://example.com/back"]}
self.assertEqual(response.status_code, 302) mock_client.provider_info = {"end_session_endpoint": "http://example.com/log_me_out"}
self.assertEqual(actual, expected) mock_client.client_id = "TEST"
# test
with less_console_noise():
response = self.client.get(reverse("logout"))
# assert
expected = (
"http://example.com/log_me_out?client_id=TEST&state"
"=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, _):
@ -194,12 +194,13 @@ class ViewsTest(TestCase):
self.assertTrue(mock_logout.called) self.assertTrue(mock_logout.called)
def test_logout_callback_redirects(self, _): def test_logout_callback_redirects(self, _):
# setup with less_console_noise():
session = self.client.session # setup
session["next"] = reverse("logout") session = self.client.session
session.save() session["next"] = reverse("logout")
# test session.save()
response = self.client.get(reverse("openid_logout_callback")) # test
# assert response = self.client.get(reverse("openid_logout_callback"))
self.assertEqual(response.status_code, 302) # assert
self.assertEqual(response.url, reverse("logout")) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse("logout"))

View file

@ -25,6 +25,8 @@ services:
- DJANGO_SECRET_KEY=really-long-random-string-BNPecI7+s8jMahQcGHZ3XQ5yUfRrSibdapVLIz0UemdktVPofDKcoy - DJANGO_SECRET_KEY=really-long-random-string-BNPecI7+s8jMahQcGHZ3XQ5yUfRrSibdapVLIz0UemdktVPofDKcoy
# Run Django in debug mode on local # Run Django in debug mode on local
- DJANGO_DEBUG=True - DJANGO_DEBUG=True
# Set DJANGO_LOG_LEVEL in env
- DJANGO_LOG_LEVEL
# Run Django without production flags # Run Django without production flags
- IS_PRODUCTION=False - IS_PRODUCTION=False
# Tell Django where it is being hosted # Tell Django where it is being hosted

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,23 +135,26 @@ 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))
# Restart the connection pool with less_console_noise():
registry.start_connection_pool() # Restart the connection pool
# Pool should be running, and be the right size registry.start_connection_pool()
self.assertEqual(registry.pool_status.connection_success, True) # Pool should be running, and be the right size
self.assertEqual(registry.pool_status.pool_running, True) self.assertEqual(registry.pool_status.connection_success, True)
self.assertEqual(registry.pool_status.pool_running, True)
# Send a command # Send a command
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
# Should this ever fail, it either means that the schema has changed, # Should this ever fail, it either means that the schema has changed,
# or the pool is broken. # or the pool is broken.
# If the schema has changed: Update the associated infoDomain.xml file # If the schema has changed: Update the associated infoDomain.xml file
self.assertEqual(result.__dict__, expected_result) self.assertEqual(result.__dict__, expected_result)
# 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,35 +201,43 @@ 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))
# Kill the connection pool with less_console_noise():
registry.kill_pool() # Start the connection pool
registry.start_connection_pool()
# Kill the connection 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
# that they cannot connect to EPP # that they cannot connect to EPP
with self.assertRaises(RegistryError): with self.assertRaises(RegistryError):
expected = "InfoDomain failed to execute due to a connection error." expected = "InfoDomain failed to execute due to a connection error."
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
self.assertEqual(result, expected)
# A subsequent command should be successful, as the pool restarts
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
self.assertEqual(result, expected) # Should this ever fail, it either means that the schema has changed,
# or the pool is broken.
# If the schema has changed: Update the associated infoDomain.xml file
self.assertEqual(result.__dict__, expected_result)
# A subsequent command should be successful, as the pool restarts # The number of open pools should match the number of requested ones.
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) # If it is 0, then they failed to open
# Should this ever fail, it either means that the schema has changed, self.assertEqual(len(registry._pool.conn), self.pool_options["size"])
# or the pool is broken. # Kill the connection pool
# If the schema has changed: Update the associated infoDomain.xml file registry.kill_pool()
self.assertEqual(result.__dict__, expected_result)
# The number of open pools should match the number of requested ones.
# If it is 0, then they failed to open
self.assertEqual(len(registry._pool.conn), self.pool_options["size"])
@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,13 +247,16 @@ 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)
self.assertEqual(registry.pool_status.pool_running, True) self.assertEqual(registry.pool_status.pool_running, True)
# Try to send a command out - should fail # Try to send a command out - should fail
with self.assertRaises(RegistryError): with self.assertRaises(RegistryError):
expected = "InfoDomain failed to execute due to a connection error." expected = "InfoDomain failed to execute due to a connection error."
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
self.assertEqual(result, expected) self.assertEqual(result, expected)

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

@ -626,7 +626,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
search_help_text = "Search by domain." search_help_text = "Search by domain."
fieldsets = [ fieldsets = [
(None, {"fields": ["creator", "domain_application"]}), (None, {"fields": ["creator", "domain_application", "notes"]}),
( (
"Type of organization", "Type of organization",
{ {
@ -677,6 +677,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",
@ -793,7 +794,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
# Detail view # Detail view
form = DomainApplicationAdminForm form = DomainApplicationAdminForm
fieldsets = [ fieldsets = [
(None, {"fields": ["status", "investigator", "creator", "approved_domain"]}), (None, {"fields": ["status", "investigator", "creator", "approved_domain", "notes"]}),
( (
"Type of organization", "Type of organization",
{ {
@ -845,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",
@ -1047,6 +1049,13 @@ class DomainAdmin(ListHeaderAdmin):
"deleted", "deleted",
] ]
fieldsets = (
(
None,
{"fields": ["name", "state", "expiration_date", "first_ready", "deleted"]},
),
)
# this ordering effects the ordering of results # this ordering effects the ordering of results
# in autocomplete_fields for domain # in autocomplete_fields for domain
ordering = ["name"] ordering = ["name"]

View file

@ -283,19 +283,20 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk,
(function (){ (function (){
// Get the current date in the format YYYY-MM-DD // Get the current date in the format YYYY-MM-DD
var currentDate = new Date().toISOString().split('T')[0]; let currentDate = new Date().toISOString().split('T')[0];
// Default the value of the start date input field to the current date // Default the value of the start date input field to the current date
let startDateInput =document.getElementById('start'); let startDateInput =document.getElementById('start');
startDateInput.value = currentDate;
// Default the value of the end date input field to the current date // Default the value of the end date input field to the current date
let endDateInput =document.getElementById('end'); let endDateInput =document.getElementById('end');
endDateInput.value = currentDate;
let exportGrowthReportButton = document.getElementById('exportLink'); let exportGrowthReportButton = document.getElementById('exportLink');
if (exportGrowthReportButton) { if (exportGrowthReportButton) {
startDateInput.value = currentDate;
endDateInput.value = currentDate;
exportGrowthReportButton.addEventListener('click', function() { exportGrowthReportButton.addEventListener('click', function() {
// Get the selected start and end dates // Get the selected start and end dates
let startDate = startDateInput.value; let startDate = startDateInput.value;

View file

@ -44,6 +44,22 @@ a.usa-button.disabled-link:focus {
color: #454545 !important color: #454545 !important
} }
a.usa-button--unstyled.disabled-link,
a.usa-button--unstyled.disabled-link:hover,
a.usa-button--unstyled.disabled-link:focus {
cursor: not-allowed !important;
outline: none !important;
text-decoration: none !important;
}
.usa-button--unstyled.disabled-button,
.usa-button--unstyled.disabled-link:hover,
.usa-button--unstyled.disabled-link:focus {
cursor: not-allowed !important;
outline: none !important;
text-decoration: none !important;
}
a.usa-button:not(.usa-button--unstyled, .usa-button--outline) { a.usa-button:not(.usa-button--unstyled, .usa-button--outline) {
color: color('white'); color: color('white');
} }

View file

@ -142,6 +142,11 @@ urlpatterns = [
views.DomainApplicationDeleteView.as_view(http_method_names=["post"]), views.DomainApplicationDeleteView.as_view(http_method_names=["post"]),
name="application-delete", name="application-delete",
), ),
path(
"domain/<int:pk>/users/<int:user_pk>/delete",
views.DomainDeleteUserView.as_view(http_method_names=["post"]),
name="domain-user-delete",
),
] ]
# we normally would guard these with `if settings.DEBUG` but tests run with # we normally would guard these with `if settings.DEBUG` but tests run with

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

View file

@ -0,0 +1,22 @@
# Generated by Django 4.2.7 on 2024-01-26 20:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("registrar", "0067_create_groups_v07"),
]
operations = [
migrations.AddField(
model_name="domainapplication",
name="notes",
field=models.TextField(blank=True, help_text="Notes about this request", null=True),
),
migrations.AddField(
model_name="domaininformation",
name="notes",
field=models.TextField(blank=True, help_text="Notes about the request", null=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,
@ -1404,7 +1405,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

@ -558,6 +558,12 @@ class DomainApplication(TimeStampedModel):
help_text="Date submitted", help_text="Date submitted",
) )
notes = models.TextField(
null=True,
blank=True,
help_text="Notes about this request",
)
def __str__(self): def __str__(self):
try: try:
if self.requested_domain and self.requested_domain.name: if self.requested_domain and self.requested_domain.name:
@ -707,7 +713,7 @@ class DomainApplication(TimeStampedModel):
# copy the information from domainapplication into domaininformation # copy the information from domainapplication into domaininformation
DomainInformation = apps.get_model("registrar.DomainInformation") DomainInformation = apps.get_model("registrar.DomainInformation")
DomainInformation.create_from_da(self, domain=created_domain) DomainInformation.create_from_da(domain_application=self, domain=created_domain)
# create the permission for the user # create the permission for the user
UserDomainRole = apps.get_model("registrar.UserDomainRole") UserDomainRole = apps.get_model("registrar.UserDomainRole")

View file

@ -1,4 +1,7 @@
from __future__ import annotations from __future__ import annotations
from django.db import transaction
from registrar.models.utility.domain_helper import DomainHelper
from .domain_application import DomainApplication from .domain_application import DomainApplication
from .utility.time_stamped_model import TimeStampedModel from .utility.time_stamped_model import TimeStampedModel
@ -202,6 +205,12 @@ class DomainInformation(TimeStampedModel):
help_text="Acknowledged .gov acceptable use policy", help_text="Acknowledged .gov acceptable use policy",
) )
notes = models.TextField(
null=True,
blank=True,
help_text="Notes about the request",
)
def __str__(self): def __str__(self):
try: try:
if self.domain and self.domain.name: if self.domain and self.domain.name:
@ -212,37 +221,63 @@ class DomainInformation(TimeStampedModel):
return "" return ""
@classmethod @classmethod
def create_from_da(cls, domain_application, domain=None): def create_from_da(cls, domain_application: DomainApplication, domain=None):
"""Takes in a DomainApplication dict and converts it into DomainInformation""" """Takes in a DomainApplication and converts it into DomainInformation"""
da_dict = domain_application.to_dict()
# remove the id so one can be assinged on creation # Throw an error if we get None - we can't create something from nothing
da_id = da_dict.pop("id", None) if domain_application is None:
raise ValueError("The provided DomainApplication is None")
# Throw an error if the da doesn't have an id
if not hasattr(domain_application, "id"):
raise ValueError("The provided DomainApplication has no id")
# check if we have a record that corresponds with the domain # check if we have a record that corresponds with the domain
# application, if so short circuit the create # application, if so short circuit the create
domain_info = cls.objects.filter(domain_application__id=da_id).first() existing_domain_info = cls.objects.filter(domain_application__id=domain_application.id).first()
if domain_info: if existing_domain_info:
return domain_info return existing_domain_info
# the following information below is not needed in the domain information:
da_dict.pop("status", None)
da_dict.pop("current_websites", None)
da_dict.pop("investigator", None)
da_dict.pop("alternative_domains", None)
da_dict.pop("requested_domain", None)
da_dict.pop("approved_domain", None)
da_dict.pop("submission_date", None)
other_contacts = da_dict.pop("other_contacts", [])
domain_info = cls(**da_dict)
domain_info.domain_application = domain_application
# Save so the object now have PK
# (needed to process the manytomany below before, first)
domain_info.save()
# Process the remaining "many to many" stuff # Get the fields that exist on both DomainApplication and DomainInformation
domain_info.other_contacts.add(*other_contacts) common_fields = DomainHelper.get_common_fields(DomainApplication, DomainInformation)
# Get a list of all many_to_many relations on DomainInformation (needs to be saved differently)
info_many_to_many_fields = DomainInformation._get_many_to_many_fields()
# Create a dictionary with only the common fields, and create a DomainInformation from it
da_dict = {}
da_many_to_many_dict = {}
for field in common_fields:
# If the field isn't many_to_many, populate the da_dict.
# If it is, populate da_many_to_many_dict as we need to save this later.
if hasattr(domain_application, field):
if field not in info_many_to_many_fields:
da_dict[field] = getattr(domain_application, field)
else:
da_many_to_many_dict[field] = getattr(domain_application, field).all()
# Create a placeholder DomainInformation object
domain_info = DomainInformation(**da_dict)
# Add the domain_application and domain fields
domain_info.domain_application = domain_application
if domain: if domain:
domain_info.domain = domain domain_info.domain = domain
domain_info.save()
# Save the instance and set the many-to-many fields.
# Lumped under .atomic to ensure we don't make redundant DB calls.
# This bundles them all together, and then saves it in a single call.
with transaction.atomic():
domain_info.save()
for field, value in da_many_to_many_dict.items():
getattr(domain_info, field).set(value)
return domain_info return domain_info
@staticmethod
def _get_many_to_many_fields():
"""Returns a set of each field.name that has the many to many relation"""
return {field.name for field in DomainInformation._meta.many_to_many} # type: ignore
class Meta: class Meta:
verbose_name_plural = "Domain information" verbose_name_plural = "Domain information"

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

@ -1,5 +1,6 @@
import re import re
from typing import Type
from django.db import models
from django import forms from django import forms
from django.http import JsonResponse from django.http import JsonResponse
@ -29,7 +30,6 @@ class DomainHelper:
@classmethod @classmethod
def validate(cls, domain: str, blank_ok=False) -> str: def validate(cls, domain: str, blank_ok=False) -> str:
"""Attempt to determine if a domain name could be requested.""" """Attempt to determine if a domain name could be requested."""
# Split into pieces for the linter # Split into pieces for the linter
domain = cls._validate_domain_string(domain, blank_ok) domain = cls._validate_domain_string(domain, blank_ok)
@ -161,3 +161,29 @@ class DomainHelper:
"""Get the top level domain. Example: `gsa.gov` -> `gov`.""" """Get the top level domain. Example: `gsa.gov` -> `gov`."""
parts = domain.rsplit(".") parts = domain.rsplit(".")
return parts[-1] if len(parts) > 1 else "" return parts[-1] if len(parts) > 1 else ""
@staticmethod
def get_common_fields(model_1: Type[models.Model], model_2: Type[models.Model]):
"""
Returns a set of field names that two Django models have in common, excluding the 'id' field.
Args:
model_1 (Type[models.Model]): The first Django model class.
model_2 (Type[models.Model]): The second Django model class.
Returns:
Set[str]: A set of field names that both models share.
Example:
If model_1 has fields {"id", "name", "color"} and model_2 has fields {"id", "color"},
the function will return {"color"}.
"""
# 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_2_fields = set(field.name for field in model_2._meta.get_fields() if field != "id")
# Get the fields that exist on both DomainApplication and DomainInformation
common_fields = model_1_fields & model_2_fields
return common_fields

View file

@ -11,7 +11,8 @@
</ul> </ul>
</p> </p>
<p>Names that <em>uniquely apply to your organization</em> are likely to be approved over names that could also apply to other organizations. In most instances, this requires including your states two-letter abbreviation.</p> <p>Names that <em>uniquely apply to your organization</em> are likely to be approved over names that could also apply to other organizations.
{% if not is_federal %}In most instances, this requires including your states two-letter abbreviation.{% endif %}</p>
<p>Requests for your organizations initials or an abbreviated name might not be approved, but we encourage you to request the name you want.</p> <p>Requests for your organizations initials or an abbreviated name might not be approved, but we encourage you to request the name you want.</p>

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

@ -3,22 +3,22 @@
{% block wrapper %} {% block wrapper %}
<div id="wrapper" class="dashboard"> <div id="wrapper" class="dashboard">
{% block messages %}
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}" {% endif %}>
{{ message }}
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}
{% block section_nav %}{% endblock %} {% block section_nav %}{% endblock %}
{% block hero %}{% endblock %} {% block hero %}{% endblock %}
{% block content %}{% endblock %} {% block content %}
{% block messages %}
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li {% if message.tags %} class="{{ message.tags }}" {% endif %}>
{{ message }}
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}
{% endblock %}
<div role="complementary">{% block complementary %}{% endblock %}</div> <div role="complementary">{% block complementary %}{% endblock %}</div>

View file

@ -56,7 +56,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

@ -16,10 +16,8 @@
<li>There is no limit to the number of domain managers you can add.</li> <li>There is no limit to the number of domain managers you can add.</li>
<li>After adding a domain manager, an email invitation will be sent to that user with <li>After adding a domain manager, an email invitation will be sent to that user with
instructions on how to set up an account.</li> instructions on how to set up an account.</li>
<li>To remove a domain manager, <a href="{% public_site_url 'contact/' %}"
target="_blank" rel="noopener noreferrer" class="usa-link">contact us</a> for
assistance.</li>
<li>All domain managers must keep their contact information updated and be responsive if contacted by the .gov team.</li> <li>All domain managers must keep their contact information updated and be responsive if contacted by the .gov team.</li>
<li>Domains must have at least one domain manager. You cant remove yourself as a domain manager if youre the only one assigned to this domain. Add another domain manager before you remove yourself from this domain.</li>
</ul> </ul>
{% if domain.permissions %} {% if domain.permissions %}
@ -30,7 +28,8 @@
<thead> <thead>
<tr> <tr>
<th data-sortable scope="col" role="columnheader">Email</th> <th data-sortable scope="col" role="columnheader">Email</th>
<th data-sortable scope="col" role="columnheader">Role</th> <th class="grid-col-2" data-sortable scope="col" role="columnheader">Role</th>
<th class="grid-col-1" scope="col" role="columnheader"><span class="sr-only">Action</span></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -40,6 +39,61 @@
{{ permission.user.email }} {{ permission.user.email }}
</th> </th>
<td data-label="Role">{{ permission.role|title }}</td> <td data-label="Role">{{ permission.role|title }}</td>
<td>
{% if can_delete_users %}
<a
id="button-toggle-user-alert-{{ forloop.counter }}"
href="#toggle-user-alert-{{ forloop.counter }}"
class="usa-button--unstyled text-no-underline"
aria-controls="toggle-user-alert-{{ forloop.counter }}"
data-open-modal
aria-disabled="false"
>
Remove
</a>
{# Display a custom message if the user is trying to delete themselves #}
{% if permission.user.email == current_user_email %}
<div
class="usa-modal"
id="toggle-user-alert-{{ forloop.counter }}"
aria-labelledby="Are you sure you want to continue?"
aria-describedby="You will be removed from this domain"
data-force-action
>
<form method="POST" action="{% url "domain-user-delete" pk=domain.id user_pk=permission.user.id %}">
{% with domain_name=domain.name|force_escape %}
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove yourself as a domain manager?" modal_description="You will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button=modal_button_self|safe %}
{% endwith %}
</form>
</div>
{% else %}
<div
class="usa-modal"
id="toggle-user-alert-{{ forloop.counter }}"
aria-labelledby="Are you sure you want to continue?"
aria-describedby="{{ permission.user.email }} will be removed"
data-force-action
>
<form method="POST" action="{% url "domain-user-delete" pk=domain.id user_pk=permission.user.id %}">
{% with email=permission.user.email|default:permission.user|force_escape domain_name=domain.name|force_escape %}
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove " heading_value=email|add:"?" modal_description="<strong>"|add:email|add:"</strong> will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button=modal_button|safe %}
{% endwith %}
</form>
</div>
{% endif %}
{% else %}
<input
type="submit"
class="usa-button--unstyled disabled-button usa-tooltip"
value="Remove"
data-position="bottom"
title="Domains must have at least one domain manager"
data-tooltip="true"
aria-disabled="true"
role="button"
>
{% endif %}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -66,8 +120,8 @@
<tr> <tr>
<th data-sortable scope="col" role="columnheader">Email</th> <th data-sortable scope="col" role="columnheader">Email</th>
<th data-sortable scope="col" role="columnheader">Date created</th> <th data-sortable scope="col" role="columnheader">Date created</th>
<th data-sortable scope="col" role="columnheader">Status</th> <th class="grid-col-2" data-sortable scope="col" role="columnheader">Status</th>
<th scope="col" role="columnheader"><span class="sr-only">Action</span></th> <th class="grid-col-1" scope="col" role="columnheader"><span class="sr-only">Action</span></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -78,8 +132,9 @@
</th> </th>
<td data-sort-value="{{ invitation.created_at|date:"U" }}" data-label="Date created">{{ invitation.created_at|date }} </td> <td data-sort-value="{{ invitation.created_at|date:"U" }}" data-label="Date created">{{ invitation.created_at|date }} </td>
<td data-label="Status">{{ invitation.status|title }}</td> <td data-label="Status">{{ invitation.status|title }}</td>
<td><form method="POST" action="{% url "invitation-delete" pk=invitation.id %}"> <td>
{% csrf_token %}<input type="submit" class="usa-button--unstyled" value="Cancel"> <form method="POST" action="{% url "invitation-delete" pk=invitation.id %}">
{% csrf_token %}<input type="submit" class="usa-button--unstyled text-no-underline" value="Cancel">
</form> </form>
</td> </td>
</tr> </tr>

View file

@ -10,7 +10,11 @@
{# the entire logged in page goes here #} {# the entire logged in page goes here #}
<div class="tablet:grid-col-11 desktop:grid-col-10 tablet:grid-offset-1"> <div class="tablet:grid-col-11 desktop:grid-col-10 tablet:grid-offset-1">
<h1>Manage your domains - Test Trigger Here</h2> {% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}
<h1>Manage your domains - TEST TEST 123</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"

View file

@ -2,7 +2,7 @@
{% for message in messages %} {% for message in messages %}
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2"> <div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2">
<div class="usa-alert__body"> <div class="usa-alert__body">
{{ message }} {{ message }}
</div> </div>
</div> </div>

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

@ -59,22 +59,22 @@ class TestDomainAdmin(MockEppLib):
""" """
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
""" """
self.client.force_login(self.superuser) with less_console_noise():
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) self.client.force_login(self.superuser)
mock_client = MockSESClient() application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
with boto3_mocking.clients.handler_for("sesv2", mock_client): mock_client = MockSESClient()
with less_console_noise(): with boto3_mocking.clients.handler_for("sesv2", mock_client):
application.approve() application.approve()
response = self.client.get("/admin/registrar/domain/") response = self.client.get("/admin/registrar/domain/")
# There are 3 template references to Federal (3) plus one reference in the table # There are 3 template references to Federal (3) plus one reference in the table
# for our actual application # for our actual application
self.assertContains(response, "Federal", count=4) self.assertContains(response, "Federal", count=4)
# This may be a bit more robust # This may be a bit more robust
self.assertContains(response, '<td class="field-organization_type">Federal</td>', count=1) self.assertContains(response, '<td class="field-organization_type">Federal</td>', count=1)
# 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")
@skip("Why did this test stop working, and is is a good test") @skip("Why did this test stop working, and is is a good test")
def test_place_and_remove_hold(self): def test_place_and_remove_hold(self):
@ -120,40 +120,37 @@ class TestDomainAdmin(MockEppLib):
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`
""" """
domain = create_ready_domain() with less_console_noise():
# Put in client hold domain = create_ready_domain()
domain.place_client_hold() # Put in client hold
p = "userpass" domain.place_client_hold()
self.client.login(username="staffuser", password=p) p = "userpass"
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),
follow=True, follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Remove from registry")
# Test the info dialog
request = self.factory.post(
"/admin/registrar/domain/{}/change/".format(domain.pk),
{"_delete_domain": "Remove from registry", "name": domain.name},
follow=True,
)
request.user = self.client
with patch("django.contrib.messages.add_message") as mock_add_message:
self.admin.do_delete_domain(request, domain)
mock_add_message.assert_called_once_with(
request,
messages.INFO,
"Domain city.gov has been deleted. Thanks!",
extra_tags="",
fail_silently=False,
) )
self.assertEqual(response.status_code, 200)
self.assertEqual(domain.state, Domain.State.DELETED) self.assertContains(response, domain.name)
self.assertContains(response, "Remove from registry")
# Test the info dialog
request = self.factory.post(
"/admin/registrar/domain/{}/change/".format(domain.pk),
{"_delete_domain": "Remove from registry", "name": domain.name},
follow=True,
)
request.user = self.client
with patch("django.contrib.messages.add_message") as mock_add_message:
self.admin.do_delete_domain(request, domain)
mock_add_message.assert_called_once_with(
request,
messages.INFO,
"Domain city.gov has been deleted. Thanks!",
extra_tags="",
fail_silently=False,
)
self.assertEqual(domain.state, Domain.State.DELETED)
def test_deletion_ready_fsm_failure(self): def test_deletion_ready_fsm_failure(self):
""" """
@ -162,38 +159,36 @@ class TestDomainAdmin(MockEppLib):
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`
""" """
domain = create_ready_domain() with less_console_noise():
p = "userpass" domain = create_ready_domain()
self.client.login(username="staffuser", password=p) p = "userpass"
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),
follow=True, follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Remove from registry")
# Test the error
request = self.factory.post(
"/admin/registrar/domain/{}/change/".format(domain.pk),
{"_delete_domain": "Remove from registry", "name": domain.name},
follow=True,
)
request.user = self.client
with patch("django.contrib.messages.add_message") as mock_add_message:
self.admin.do_delete_domain(request, domain)
mock_add_message.assert_called_once_with(
request,
messages.ERROR,
"Error deleting this Domain: "
"Can't switch from state 'ready' to 'deleted'"
", must be either 'dns_needed' or 'on_hold'",
extra_tags="",
fail_silently=False,
) )
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Remove from registry")
# Test the error
request = self.factory.post(
"/admin/registrar/domain/{}/change/".format(domain.pk),
{"_delete_domain": "Remove from registry", "name": domain.name},
follow=True,
)
request.user = self.client
with patch("django.contrib.messages.add_message") as mock_add_message:
self.admin.do_delete_domain(request, domain)
mock_add_message.assert_called_once_with(
request,
messages.ERROR,
"Error deleting this Domain: "
"Can't switch from state 'ready' to 'deleted'"
", must be either 'dns_needed' or 'on_hold'",
extra_tags="",
fail_silently=False,
)
self.assertEqual(domain.state, Domain.State.READY) self.assertEqual(domain.state, Domain.State.READY)
@ -205,62 +200,57 @@ class TestDomainAdmin(MockEppLib):
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
""" """
domain = create_ready_domain() with less_console_noise():
# Put in client hold domain = create_ready_domain()
domain.place_client_hold() # Put in client hold
p = "userpass" domain.place_client_hold()
self.client.login(username="staffuser", password=p) p = "userpass"
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),
follow=True, follow=True,
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, domain.name)
self.assertContains(response, "Remove from registry")
# Test the info dialog
request = self.factory.post(
"/admin/registrar/domain/{}/change/".format(domain.pk),
{"_delete_domain": "Remove from registry", "name": domain.name},
follow=True,
)
request.user = self.client
# Delete it once
with patch("django.contrib.messages.add_message") as mock_add_message:
self.admin.do_delete_domain(request, domain)
mock_add_message.assert_called_once_with(
request,
messages.INFO,
"Domain city.gov has been deleted. Thanks!",
extra_tags="",
fail_silently=False,
) )
self.assertEqual(response.status_code, 200)
self.assertEqual(domain.state, Domain.State.DELETED) self.assertContains(response, domain.name)
self.assertContains(response, "Remove from registry")
# Try to delete it again # 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), {"_delete_domain": "Remove from registry", "name": domain.name},
{"_delete_domain": "Remove from registry", "name": domain.name}, follow=True,
follow=True,
)
request.user = self.client
with patch("django.contrib.messages.add_message") as mock_add_message:
self.admin.do_delete_domain(request, domain)
mock_add_message.assert_called_once_with(
request,
messages.INFO,
"This domain is already deleted",
extra_tags="",
fail_silently=False,
) )
request.user = self.client
# Delete it once
with patch("django.contrib.messages.add_message") as mock_add_message:
self.admin.do_delete_domain(request, domain)
mock_add_message.assert_called_once_with(
request,
messages.INFO,
"Domain city.gov has been deleted. Thanks!",
extra_tags="",
fail_silently=False,
)
self.assertEqual(domain.state, Domain.State.DELETED) self.assertEqual(domain.state, Domain.State.DELETED)
# Try to delete it again
# Test the info dialog
request = self.factory.post(
"/admin/registrar/domain/{}/change/".format(domain.pk),
{"_delete_domain": "Remove from registry", "name": domain.name},
follow=True,
)
request.user = self.client
with patch("django.contrib.messages.add_message") as mock_add_message:
self.admin.do_delete_domain(request, domain)
mock_add_message.assert_called_once_with(
request,
messages.INFO,
"This domain is already deleted",
extra_tags="",
fail_silently=False,
)
self.assertEqual(domain.state, Domain.State.DELETED)
@skip("Waiting on epp lib to implement") @skip("Waiting on epp lib to implement")
def test_place_and_remove_hold_epp(self): def test_place_and_remove_hold_epp(self):
@ -624,6 +614,7 @@ class TestDomainApplicationAdmin(MockEppLib):
"anything_else", "anything_else",
"is_policy_acknowledged", "is_policy_acknowledged",
"submission_date", "submission_date",
"notes",
"current_websites", "current_websites",
"other_contacts", "other_contacts",
"alternative_domains", "alternative_domains",
@ -641,6 +632,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",
@ -1066,7 +1058,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()
@ -1074,6 +1066,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(
@ -1117,6 +1110,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"
@ -1281,64 +1295,62 @@ class ListHeaderAdminTest(TestCase):
self.superuser = create_superuser() self.superuser = create_superuser()
def test_changelist_view(self): def test_changelist_view(self):
# Have to get creative to get past linter with less_console_noise():
p = "adminpass" # Have to get creative to get past linter
self.client.login(username="superuser", password=p) p = "adminpass"
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 response = self.client.get(
response = self.client.get( "/admin/registrar/domainapplication/",
"/admin/registrar/domainapplication/",
{
"status__exact": "started",
"investigator__id__exact": user.id,
"q": "Hello",
},
follow=True,
)
# Assert that the filters and search_query are added to the extra_context
self.assertIn("filters", response.context)
self.assertIn("search_query", response.context)
# Assert the content of filters and search_query
filters = response.context["filters"]
search_query = response.context["search_query"]
self.assertEqual(search_query, "Hello")
self.assertEqual(
filters,
[
{"parameter_name": "status", "parameter_value": "started"},
{ {
"parameter_name": "investigator", "status__exact": "started",
"parameter_value": user.first_name + " " + user.last_name, "investigator__id__exact": user.id,
"q": "Hello",
}, },
], follow=True,
) )
# Assert that the filters and search_query are added to the extra_context
self.assertIn("filters", response.context)
self.assertIn("search_query", response.context)
# Assert the content of filters and search_query
filters = response.context["filters"]
search_query = response.context["search_query"]
self.assertEqual(search_query, "Hello")
self.assertEqual(
filters,
[
{"parameter_name": "status", "parameter_value": "started"},
{
"parameter_name": "investigator",
"parameter_value": user.first_name + " " + user.last_name,
},
],
)
def test_get_filters(self): def test_get_filters(self):
# Create a mock request object with less_console_noise():
request = self.factory.get("/admin/yourmodel/") # Create a mock request object
# Set the GET parameters for testing request = self.factory.get("/admin/yourmodel/")
request.GET = { # Set the GET parameters for testing
"status": "started", request.GET = {
"investigator": "Jeff Lebowski", "status": "started",
"q": "search_value", "investigator": "Jeff Lebowski",
} "q": "search_value",
# Call the get_filters method }
filters = self.admin.get_filters(request) # Call the get_filters method
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,
[ [
{"parameter_name": "status", "parameter_value": "started"}, {"parameter_name": "status", "parameter_value": "started"},
{"parameter_name": "investigator", "parameter_value": "Jeff Lebowski"}, {"parameter_name": "investigator", "parameter_value": "Jeff Lebowski"},
], ],
) )
def tearDown(self): def tearDown(self):
# delete any applications too # delete any applications too
@ -1777,42 +1789,38 @@ 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) application1 = completed_application(submitter=contact, name="city1.gov")
application1 = completed_application(submitter=contact, name="city1.gov") application2 = completed_application(submitter=contact, name="city2.gov")
application2 = completed_application(submitter=contact, name="city2.gov") 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:
# Use the test client to simulate the request
with patch("django.contrib.messages.warning") as mock_warning: response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk]))
# Use the test client to simulate the request logger.debug(mock_warning)
response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk])) # Assert that the error message was called with the correct argument
# Note: The 6th join will be a user.
logger.info(mock_warning) mock_warning.assert_called_once_with(
response.wsgi_request,
# Assert that the error message was called with the correct argument "<ul class='messagelist_content-list--unstyled'>"
# Note: The 6th join will be a user. "<li>Joined to DomainApplication: <a href='/admin/registrar/"
mock_warning.assert_called_once_with( f"domainapplication/{application1.pk}/change/'>city1.gov</a></li>"
response.wsgi_request, "<li>Joined to DomainApplication: <a href='/admin/registrar/"
"<ul class='messagelist_content-list--unstyled'>" f"domainapplication/{application2.pk}/change/'>city2.gov</a></li>"
"<li>Joined to DomainApplication: <a href='/admin/registrar/" "<li>Joined to DomainApplication: <a href='/admin/registrar/"
f"domainapplication/{application1.pk}/change/'>city1.gov</a></li>" f"domainapplication/{application3.pk}/change/'>city3.gov</a></li>"
"<li>Joined to DomainApplication: <a href='/admin/registrar/" "<li>Joined to DomainApplication: <a href='/admin/registrar/"
f"domainapplication/{application2.pk}/change/'>city2.gov</a></li>" f"domainapplication/{application4.pk}/change/'>city4.gov</a></li>"
"<li>Joined to DomainApplication: <a href='/admin/registrar/" "<li>Joined to DomainApplication: <a href='/admin/registrar/"
f"domainapplication/{application3.pk}/change/'>city3.gov</a></li>" f"domainapplication/{application5.pk}/change/'>city5.gov</a></li>"
"<li>Joined to DomainApplication: <a href='/admin/registrar/" "</ul>"
f"domainapplication/{application4.pk}/change/'>city4.gov</a></li>" "<p class='font-sans-3xs'>And 1 more...</p>",
"<li>Joined to DomainApplication: <a href='/admin/registrar/" )
f"domainapplication/{application5.pk}/change/'>city5.gov</a></li>"
"</ul>"
"<p class='font-sans-3xs'>And 1 more...</p>",
)
def tearDown(self): def tearDown(self):
DomainApplication.objects.all().delete() DomainApplication.objects.all().delete()

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,122 +52,103 @@ 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 patch( with less_console_noise():
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa with patch(
return_value=True, "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
): return_value=True,
call_command("populate_first_ready") ):
call_command("populate_first_ready")
def test_populate_first_ready_state_ready(self): def test_populate_first_ready_state_ready(self):
""" """
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'
""" """
# Set the created at date with less_console_noise():
self.ready_domain.created_at = self.ready_at_date # Set the created at date
self.ready_domain.save() self.ready_domain.created_at = self.ready_at_date_tz_aware
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
self.run_populate_first_ready()
# Run the expiration date script self.assertEqual(desired_domain, self.ready_domain)
self.run_populate_first_ready() # Explicitly test the first_ready date
first_ready = Domain.objects.filter(name="fakeready.gov").get().first_ready
self.assertEqual(desired_domain, self.ready_domain) self.assertEqual(first_ready, self.ready_at_date)
# Explicitly test the first_ready date
first_ready = Domain.objects.filter(name="fakeready.gov").get().first_ready
self.assertEqual(first_ready, self.ready_at_date)
def test_populate_first_ready_state_deleted(self): def test_populate_first_ready_state_deleted(self):
""" """
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'
""" """
# Set the created at date with less_console_noise():
self.deleted_domain.created_at = self.ready_at_date # Set the created at date
self.deleted_domain.save() self.deleted_domain.created_at = self.ready_at_date_tz_aware
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
self.run_populate_first_ready()
# Run the expiration date script self.assertEqual(desired_domain, self.deleted_domain)
self.run_populate_first_ready() # Explicitly test the first_ready date
first_ready = Domain.objects.filter(name="fakedeleted.gov").get().first_ready
self.assertEqual(desired_domain, self.deleted_domain) self.assertEqual(first_ready, self.ready_at_date)
# Explicitly test the first_ready date
first_ready = Domain.objects.filter(name="fakedeleted.gov").get().first_ready
self.assertEqual(first_ready, self.ready_at_date)
def test_populate_first_ready_state_dns_needed(self): def test_populate_first_ready_state_dns_needed(self):
""" """
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'
""" """
# Set the created at date with less_console_noise():
self.dns_needed_domain.created_at = self.ready_at_date # Set the created at date
self.dns_needed_domain.save() self.dns_needed_domain.created_at = self.ready_at_date_tz_aware
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
self.run_populate_first_ready()
# Run the expiration date script current_domain = self.dns_needed_domain
self.run_populate_first_ready() # The object should largely be unaltered (does not test first_ready)
self.assertEqual(desired_domain, current_domain)
current_domain = self.dns_needed_domain first_ready = Domain.objects.filter(name="fakedns.gov").get().first_ready
# The object should largely be unaltered (does not test first_ready) # Explicitly test the first_ready date
self.assertEqual(desired_domain, current_domain) self.assertNotEqual(first_ready, self.ready_at_date)
self.assertEqual(first_ready, None)
first_ready = Domain.objects.filter(name="fakedns.gov").get().first_ready
# Explicitly test the first_ready date
self.assertNotEqual(first_ready, self.ready_at_date)
self.assertEqual(first_ready, None)
def test_populate_first_ready_state_on_hold(self): def test_populate_first_ready_state_on_hold(self):
""" """
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.save() self.hold_domain.created_at = self.ready_at_date_tz_aware
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
self.assertEqual(desired_domain, current_domain)
current_domain = self.hold_domain # Explicitly test the first_ready date
self.assertEqual(desired_domain, current_domain) first_ready = Domain.objects.filter(name="fakehold.gov").get().first_ready
self.assertEqual(first_ready, self.ready_at_date)
# Explicitly test the first_ready date
first_ready = Domain.objects.filter(name="fakehold.gov").get().first_ready
self.assertEqual(first_ready, self.ready_at_date)
def test_populate_first_ready_state_unknown(self): def test_populate_first_ready_state_unknown(self):
""" """
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'
""" """
# Set the created at date with less_console_noise():
self.unknown_domain.created_at = self.ready_at_date # Set the created at date
self.unknown_domain.save() self.unknown_domain.created_at = self.ready_at_date_tz_aware
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
# The object should largely be unaltered (does not test first_ready)
current_domain = self.unknown_domain self.assertEqual(desired_domain, current_domain)
# Explicitly test the first_ready date
# The object should largely be unaltered (does not test first_ready) first_ready = Domain.objects.filter(name="fakeunknown.gov").get().first_ready
self.assertEqual(desired_domain, current_domain) self.assertNotEqual(first_ready, self.ready_at_date)
self.assertEqual(first_ready, None)
# Explicitly test the first_ready date
first_ready = Domain.objects.filter(name="fakeunknown.gov").get().first_ready
self.assertNotEqual(first_ready, self.ready_at_date)
self.assertEqual(first_ready, None)
class TestPatchAgencyInfo(TestCase): class TestPatchAgencyInfo(TestCase):
@ -185,7 +169,8 @@ 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"""
call_command("patch_federal_agency_info", "registrar/tests/data/fake_current_full.csv", debug=True) with less_console_noise():
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,17 +179,14 @@ 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
self.domain_info.refresh_from_db()
# Reload the domain_info object from the database # Check that the federal_agency field was updated
self.domain_info.refresh_from_db() self.assertEqual(self.domain_info.federal_agency, "test agency")
# Check that the federal_agency field was updated
self.assertEqual(self.domain_info.federal_agency, "test agency")
def test_patch_agency_info_skip(self): def test_patch_agency_info_skip(self):
""" """
@ -213,21 +195,18 @@ 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.
""" """
# Set federal_agency to None to simulate a skip with less_console_noise():
self.transition_domain.federal_agency = None # Set federal_agency to None to simulate a skip
self.transition_domain.save() self.transition_domain.federal_agency = None
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
self.domain_info.refresh_from_db()
# Reload the domain_info object from the database # Check that the federal_agency field was not updated
self.domain_info.refresh_from_db() self.assertIsNone(self.domain_info.federal_agency)
# Check that the federal_agency field was not updated
self.assertIsNone(self.domain_info.federal_agency)
def test_patch_agency_info_skip_updates_data(self): def test_patch_agency_info_skip_updates_data(self):
""" """
@ -235,25 +214,21 @@ 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.
""" """
# Set federal_agency to None to simulate a skip with less_console_noise():
self.transition_domain.federal_agency = None # Set federal_agency to None to simulate a skip
self.transition_domain.save() self.transition_domain.federal_agency = None
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
self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0])
# Check that the correct log message was output # Reload the domain_info object from the database
self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0]) self.domain_info.refresh_from_db()
# Check that the federal_agency field was not updated
# Reload the domain_info object from the database self.assertEqual(self.domain_info.federal_agency, "World War I Centennial Commission")
self.domain_info.refresh_from_db()
# Check that the federal_agency field was not updated
self.assertEqual(self.domain_info.federal_agency, "World War I Centennial Commission")
def test_patch_agency_info_skips_valid_domains(self): def test_patch_agency_info_skips_valid_domains(self):
""" """
@ -261,20 +236,17 @@ 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
""" """
self.domain_info.federal_agency = "unchanged" with less_console_noise():
self.domain_info.save() self.domain_info.federal_agency = "unchanged"
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
self.domain_info.refresh_from_db()
# Reload the domain_info object from the database # Check that the federal_agency field was not updated
self.domain_info.refresh_from_db() self.assertEqual(self.domain_info.federal_agency, "unchanged")
# Check that the federal_agency field was not updated
self.assertEqual(self.domain_info.federal_agency, "unchanged")
class TestExtendExpirationDates(MockEppLib): class TestExtendExpirationDates(MockEppLib):
@ -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,83 +308,82 @@ 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 patch( with less_console_noise():
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa with patch(
return_value=True, "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
): return_value=True,
call_command("extend_expiration_dates") ):
call_command("extend_expiration_dates")
def test_extends_expiration_date_correctly(self): def test_extends_expiration_date_correctly(self):
""" """
Tests that the extend_expiration_dates method extends dates as expected Tests that the extend_expiration_dates method extends dates as expected
""" """
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get() with less_console_noise():
desired_domain.expiration_date = datetime.date(2024, 11, 15) desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
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)
# Explicitly test the expiration date
self.assertEqual(desired_domain, current_domain) self.assertEqual(current_domain.expiration_date, date(2024, 11, 15))
# Explicitly test the expiration date
self.assertEqual(current_domain.expiration_date, datetime.date(2024, 11, 15))
def test_extends_expiration_date_skips_non_current(self): 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.
""" """
desired_domain = Domain.objects.filter(name="fake.gov").get() with less_console_noise():
desired_domain.expiration_date = datetime.date(2022, 5, 25) desired_domain = Domain.objects.filter(name="fake.gov").get()
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
# will skip all dates less than date(2023, 11, 15), meaning that this domain
# Explicitly test the expiration date. The extend_expiration_dates script # should not be affected by the change.
# will skip all dates less than date(2023, 11, 15), meaning that this domain self.assertEqual(current_domain.expiration_date, date(2022, 5, 25))
# should not be affected by the change.
self.assertEqual(current_domain.expiration_date, datetime.date(2022, 5, 25))
def test_extends_expiration_date_skips_maximum_date(self): 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.
""" """
desired_domain = Domain.objects.filter(name="fakemaximum.gov").get() with less_console_noise():
desired_domain.expiration_date = datetime.date(2024, 12, 31) desired_domain = Domain.objects.filter(name="fakemaximum.gov").get()
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()
current_domain = Domain.objects.filter(name="fakemaximum.gov").get() current_domain = Domain.objects.filter(name="fakemaximum.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(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"
""" """
desired_domain = Domain.objects.filter(name="fakeneeded.gov").get() with less_console_noise():
desired_domain.expiration_date = datetime.date(2023, 11, 15) desired_domain = Domain.objects.filter(name="fakeneeded.gov").get()
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()
current_domain = Domain.objects.filter(name="fakeneeded.gov").get() current_domain = Domain.objects.filter(name="fakeneeded.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(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.
""" """
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get() with less_console_noise():
desired_domain.expiration_date = datetime.date(2024, 11, 15) desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
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
self.assertEqual(desired_domain.expiration_date, date(2024, 11, 15))
# Explicitly test the expiration date # Run the expiration date script again
self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15)) self.run_extend_expiration_dates()
# The old domain shouldn't have changed
# Run the expiration date script again self.assertEqual(desired_domain, current_domain)
self.run_extend_expiration_dates() # Explicitly test the expiration date - should be the same
self.assertEqual(desired_domain.expiration_date, date(2024, 11, 15))
# The old domain shouldn't have changed
self.assertEqual(desired_domain, current_domain)
# Explicitly test the expiration date - should be the same
self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15))
class TestDiscloseEmails(MockEppLib): class TestDiscloseEmails(MockEppLib):
@ -461,39 +425,41 @@ 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 patch( with less_console_noise():
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa with patch(
return_value=True, "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
): return_value=True,
call_command("disclose_security_emails") ):
call_command("disclose_security_emails")
def test_disclose_security_emails(self): def test_disclose_security_emails(self):
""" """
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.
""" """
domain, _ = Domain.objects.get_or_create(name="testdisclose.gov", state=Domain.State.READY) with less_console_noise():
expectedSecContact = PublicContact.get_default_security() domain, _ = Domain.objects.get_or_create(name="testdisclose.gov", state=Domain.State.READY)
expectedSecContact.domain = domain expectedSecContact = PublicContact.get_default_security()
expectedSecContact.email = "123@mail.gov" expectedSecContact.domain = domain
# set domain security email to 123@mail.gov instead of default email expectedSecContact.email = "123@mail.gov"
domain.security_contact = expectedSecContact # set domain security email to 123@mail.gov instead of default email
self.run_disclose_security_emails() domain.security_contact = expectedSecContact
self.run_disclose_security_emails()
# running disclose_security_emails sends EPP call UpdateContact with disclose # running disclose_security_emails sends EPP call UpdateContact with disclose
self.mockedSendFunction.assert_has_calls( self.mockedSendFunction.assert_has_calls(
[ [
call( call(
commands.UpdateContact( commands.UpdateContact(
id=domain.security_contact.registry_id, id=domain.security_contact.registry_id,
postal_info=domain._make_epp_contact_postal_info(contact=domain.security_contact), postal_info=domain._make_epp_contact_postal_info(contact=domain.security_contact),
email=domain.security_contact.email, email=domain.security_contact.email,
voice=domain.security_contact.voice, voice=domain.security_contact.voice,
fax=domain.security_contact.fax, fax=domain.security_contact.fax,
auth_info=common.ContactAuthInfo(pw="2fooBAR123fooBaz"), auth_info=common.ContactAuthInfo(pw="2fooBAR123fooBaz"),
disclose=domain._disclose_fields(contact=domain.security_contact), disclose=domain._disclose_fields(contact=domain.security_contact),
), ),
cleaned=True, cleaned=True,
) )
] ]
) )

View file

@ -60,127 +60,134 @@ 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."""
return self.assertRaises(Exception, None, exception_type) with less_console_noise():
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 self.assertRaisesRegex(IntegrityError, "creator"): with less_console_noise():
DomainApplication.objects.create() with self.assertRaisesRegex(IntegrityError, "creator"):
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."""
user, _ = User.objects.get_or_create(username="testy") with less_console_noise():
application = DomainApplication.objects.create(creator=user) user, _ = User.objects.get_or_create(username="testy")
self.assertEqual(application.status, DomainApplication.ApplicationStatus.STARTED) application = DomainApplication.objects.create(creator=user)
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."""
user, _ = User.objects.get_or_create(username="testy") with less_console_noise():
contact = Contact.objects.create() user, _ = User.objects.get_or_create(username="testy")
com_website, _ = Website.objects.get_or_create(website="igorville.com") contact = Contact.objects.create()
gov_website, _ = Website.objects.get_or_create(website="igorville.gov") com_website, _ = Website.objects.get_or_create(website="igorville.com")
domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") gov_website, _ = Website.objects.get_or_create(website="igorville.gov")
application = DomainApplication.objects.create( domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
creator=user, application = DomainApplication.objects.create(
investigator=user, creator=user,
organization_type=DomainApplication.OrganizationChoices.FEDERAL, investigator=user,
federal_type=DomainApplication.BranchChoices.EXECUTIVE, organization_type=DomainApplication.OrganizationChoices.FEDERAL,
is_election_board=False, federal_type=DomainApplication.BranchChoices.EXECUTIVE,
organization_name="Test", is_election_board=False,
address_line1="100 Main St.", organization_name="Test",
address_line2="APT 1A", address_line1="100 Main St.",
state_territory="CA", address_line2="APT 1A",
zipcode="12345-6789", state_territory="CA",
authorizing_official=contact, zipcode="12345-6789",
requested_domain=domain, authorizing_official=contact,
submitter=contact, requested_domain=domain,
purpose="Igorville rules!", submitter=contact,
anything_else="All of Igorville loves the dotgov program.", purpose="Igorville rules!",
is_policy_acknowledged=True, anything_else="All of Igorville loves the dotgov program.",
) is_policy_acknowledged=True,
application.current_websites.add(com_website) )
application.alternative_domains.add(gov_website) application.current_websites.add(com_website)
application.other_contacts.add(contact) application.alternative_domains.add(gov_website)
application.save() application.other_contacts.add(contact)
application.save()
def test_domain_info(self): def test_domain_info(self):
"""Can create domain info with all fields.""" """Can create domain info with all fields."""
user, _ = User.objects.get_or_create(username="testy") with less_console_noise():
contact = Contact.objects.create() user, _ = User.objects.get_or_create(username="testy")
domain, _ = Domain.objects.get_or_create(name="igorville.gov") contact = Contact.objects.create()
information = DomainInformation.objects.create( domain, _ = Domain.objects.get_or_create(name="igorville.gov")
creator=user, information = DomainInformation.objects.create(
organization_type=DomainInformation.OrganizationChoices.FEDERAL, creator=user,
federal_type=DomainInformation.BranchChoices.EXECUTIVE, organization_type=DomainInformation.OrganizationChoices.FEDERAL,
is_election_board=False, federal_type=DomainInformation.BranchChoices.EXECUTIVE,
organization_name="Test", is_election_board=False,
address_line1="100 Main St.", organization_name="Test",
address_line2="APT 1A", address_line1="100 Main St.",
state_territory="CA", address_line2="APT 1A",
zipcode="12345-6789", state_territory="CA",
authorizing_official=contact, zipcode="12345-6789",
submitter=contact, authorizing_official=contact,
purpose="Igorville rules!", submitter=contact,
anything_else="All of Igorville loves the dotgov program.", purpose="Igorville rules!",
is_policy_acknowledged=True, anything_else="All of Igorville loves the dotgov program.",
domain=domain, is_policy_acknowledged=True,
) domain=domain,
information.other_contacts.add(contact) )
information.save() information.other_contacts.add(contact)
self.assertEqual(information.domain.id, domain.id) information.save()
self.assertEqual(information.id, domain.domain_info.id) self.assertEqual(information.domain.id, domain.id)
self.assertEqual(information.id, domain.domain_info.id)
def test_status_fsm_submit_fail(self): def test_status_fsm_submit_fail(self):
user, _ = User.objects.get_or_create(username="testy") with less_console_noise():
application = DomainApplication.objects.create(creator=user) user, _ = User.objects.get_or_create(username="testy")
application = DomainApplication.objects.create(creator=user)
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():
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
# can't submit an application with a null domain name # can't submit an application with a null domain name
application.submit() application.submit()
def test_status_fsm_submit_succeed(self): def test_status_fsm_submit_succeed(self):
user, _ = User.objects.get_or_create(username="testy") with less_console_noise():
site = DraftDomain.objects.create(name="igorville.gov") user, _ = User.objects.get_or_create(username="testy")
application = DomainApplication.objects.create(creator=user, requested_domain=site) site = DraftDomain.objects.create(name="igorville.gov")
application = DomainApplication.objects.create(creator=user, requested_domain=site)
# no submitter email so this emits a log warning # no submitter email so this emits a log warning
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() application.submit()
self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED) self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED)
def test_submit_sends_email(self): def test_submit_sends_email(self):
"""Create an application and submit it and see if email was sent.""" """Create an application and submit it and see if email was sent."""
user, _ = User.objects.get_or_create(username="testy") with less_console_noise():
contact = Contact.objects.create(email="test@test.gov") user, _ = User.objects.get_or_create(username="testy")
domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") contact = Contact.objects.create(email="test@test.gov")
application = DomainApplication.objects.create( domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
creator=user, application = DomainApplication.objects.create(
requested_domain=domain, creator=user,
submitter=contact, requested_domain=domain,
) submitter=contact,
application.save() )
application.save()
with boto3_mocking.clients.handler_for("sesv2", self.mock_client): with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
with less_console_noise():
application.submit() application.submit()
# check to see if an email was sent # check to see if an email was sent
self.assertGreater( self.assertGreater(
len( 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 "test@test.gov" in email["kwargs"]["Destination"]["ToAddresses"]
] ]
), ),
0, 0,
) )
def test_submit_transition_allowed(self): def test_submit_transition_allowed(self):
""" """
@ -268,13 +275,13 @@ 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:
application.action_needed() application.action_needed()
except TransitionNotAllowed: except TransitionNotAllowed:
self.fail("TransitionNotAllowed was raised, but it was not expected.") self.fail("TransitionNotAllowed was raised, but it was not expected.")
def test_action_needed_transition_not_allowed(self): def test_action_needed_transition_not_allowed(self):
""" """
@ -286,11 +293,11 @@ 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):
application.action_needed() application.action_needed()
def test_approved_transition_allowed(self): def test_approved_transition_allowed(self):
""" """
@ -499,25 +506,29 @@ 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"""
self.started_application.no_other_contacts_rationale = "You talkin' to me?" with less_console_noise():
self.started_application.save() self.started_application.no_other_contacts_rationale = "You talkin' to me?"
self.assertEquals(self.started_application.has_rationale(), True) self.started_application.save()
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"""
self.assertEquals(self.started_application.has_rationale(), False) with less_console_noise():
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"""
# completed_application has other contacts by default with less_console_noise():
self.assertEquals(self.started_application.has_other_contacts(), True) # completed_application has other contacts by default
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"""
application = completed_application( with less_console_noise():
status=DomainApplication.ApplicationStatus.STARTED, name="no-others.gov", has_other_contacts=False application = completed_application(
) status=DomainApplication.ApplicationStatus.STARTED, name="no-others.gov", has_other_contacts=False
self.assertEquals(application.has_other_contacts(), False) )
self.assertEquals(application.has_other_contacts(), False)
class TestPermissions(TestCase): class TestPermissions(TestCase):
@ -548,9 +559,9 @@ class TestPermissions(TestCase):
self.assertTrue(UserDomainRole.objects.get(user=user, domain=domain)) self.assertTrue(UserDomainRole.objects.get(user=user, domain=domain))
class TestDomainInfo(TestCase): class TestDomainInformation(TestCase):
"""Test creation of Domain Information when approved.""" """Test the DomainInformation model, when approved or otherwise"""
def setUp(self): def setUp(self):
super().setUp() super().setUp()
@ -559,12 +570,18 @@ class TestDomainInfo(TestCase):
def tearDown(self): def tearDown(self):
super().tearDown() super().tearDown()
self.mock_client.EMAILS_SENT.clear() self.mock_client.EMAILS_SENT.clear()
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainApplication.objects.all().delete()
User.objects.all().delete()
DraftDomain.objects.all().delete()
@boto3_mocking.patching @boto3_mocking.patching
def test_approval_creates_info(self): def test_approval_creates_info(self):
self.maxDiff = None
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
user, _ = User.objects.get_or_create() user, _ = User.objects.get_or_create()
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain) application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain, notes="test notes")
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():
@ -574,7 +591,25 @@ class TestDomainInfo(TestCase):
# should be an information present for this domain # should be an information present for this domain
domain = Domain.objects.get(name="igorville.gov") domain = Domain.objects.get(name="igorville.gov")
self.assertTrue(DomainInformation.objects.get(domain=domain)) domain_information = DomainInformation.objects.filter(domain=domain)
self.assertTrue(domain_information.exists())
# Test that both objects are what we expect
current_domain_information = domain_information.get().__dict__
expected_domain_information = DomainInformation(
creator=user,
domain=domain,
notes="test notes",
domain_application=application,
).__dict__
# Test the two records for consistency
self.assertEqual(self.clean_dict(current_domain_information), self.clean_dict(expected_domain_information))
def clean_dict(self, dict_obj):
"""Cleans dynamic fields in a dictionary"""
bad_fields = ["_state", "created_at", "id", "updated_at"]
return {k: v for k, v in dict_obj.items() if k not in bad_fields}
class TestInvitations(TestCase): class TestInvitations(TestCase):

File diff suppressed because it is too large Load diff

View file

@ -23,6 +23,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,41 +81,43 @@ 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"""
mock_client = MagicMock() with less_console_noise():
fake_open = mock_open() mock_client = MagicMock()
expected_file_content = [ fake_open = mock_open()
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"), expected_file_content = [
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"), call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"), call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
] call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
# We don't actually want to write anything for a test case, ]
# we just want to verify what is being written. # We don't actually want to write anything for a test case,
with boto3_mocking.clients.handler_for("s3", mock_client): # we just want to verify what is being written.
with patch("builtins.open", fake_open): with boto3_mocking.clients.handler_for("s3", mock_client):
call_command("generate_current_federal_report", checkpath=False) with patch("builtins.open", fake_open):
content = fake_open() call_command("generate_current_federal_report", checkpath=False)
content = fake_open()
content.write.assert_has_calls(expected_file_content) content.write.assert_has_calls(expected_file_content)
@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"""
mock_client = MagicMock() with less_console_noise():
fake_open = mock_open() mock_client = MagicMock()
expected_file_content = [ fake_open = mock_open()
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"), expected_file_content = [
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"), call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"), call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
call("adomain2.gov,Interstate,,,,, \r\n"), call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
] call("adomain2.gov,Interstate,,,,, \r\n"),
# We don't actually want to write anything for a test case, ]
# we just want to verify what is being written. # We don't actually want to write anything for a test case,
with boto3_mocking.clients.handler_for("s3", mock_client): # we just want to verify what is being written.
with patch("builtins.open", fake_open): with boto3_mocking.clients.handler_for("s3", mock_client):
call_command("generate_current_full_report", checkpath=False) with patch("builtins.open", fake_open):
content = fake_open() call_command("generate_current_full_report", checkpath=False)
content = fake_open()
content.write.assert_has_calls(expected_file_content) content.write.assert_has_calls(expected_file_content)
@boto3_mocking.patching @boto3_mocking.patching
def test_not_found_full_report(self): def test_not_found_full_report(self):
@ -123,19 +126,20 @@ 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")
mock_client = MagicMock() with less_console_noise():
mock_client.get_object.side_effect = side_effect mock_client = MagicMock()
mock_client.get_object.side_effect = side_effect
response = None response = None
with boto3_mocking.clients.handler_for("s3", mock_client): with boto3_mocking.clients.handler_for("s3", mock_client):
with patch("boto3.client", return_value=mock_client): with patch("boto3.client", return_value=mock_client):
with self.assertRaises(S3ClientError) as context: with self.assertRaises(S3ClientError) as context:
response = self.client.get("/api/v1/get-report/current-full") response = self.client.get("/api/v1/get-report/current-full")
# Check that the response has status code 500 # Check that the response has status code 500
self.assertEqual(response.status_code, 500) self.assertEqual(response.status_code, 500)
# Check that we get the right error back from the page # Check that we get the right error back from the page
self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR) self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR)
@boto3_mocking.patching @boto3_mocking.patching
def test_not_found_federal_report(self): def test_not_found_federal_report(self):
@ -144,83 +148,86 @@ 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")
mock_client = MagicMock() with less_console_noise():
mock_client.get_object.side_effect = side_effect mock_client = MagicMock()
mock_client.get_object.side_effect = side_effect
with boto3_mocking.clients.handler_for("s3", mock_client): with boto3_mocking.clients.handler_for("s3", mock_client):
with patch("boto3.client", return_value=mock_client): with patch("boto3.client", return_value=mock_client):
with self.assertRaises(S3ClientError) as context: with self.assertRaises(S3ClientError) as context:
response = self.client.get("/api/v1/get-report/current-federal") response = self.client.get("/api/v1/get-report/current-federal")
# Check that the response has status code 500 # Check that the response has status code 500
self.assertEqual(response.status_code, 500) self.assertEqual(response.status_code, 500)
# Check that we get the right error back from the page # Check that we get the right error back from the page
self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR) self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR)
@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"""
mock_client = MagicMock() with less_console_noise():
mock_client_instance = mock_client.return_value mock_client = MagicMock()
mock_client_instance = mock_client.return_value
with open("registrar/tests/data/fake_current_federal.csv", "r") as file: with open("registrar/tests/data/fake_current_federal.csv", "r") as file:
file_content = file.read() file_content = file.read()
# Mock a recieved file # Mock a recieved file
mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())} mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())}
with boto3_mocking.clients.handler_for("s3", mock_client): with boto3_mocking.clients.handler_for("s3", mock_client):
request = self.factory.get("/fake-path") request = self.factory.get("/fake-path")
response = get_current_federal(request) response = get_current_federal(request)
# Check that we are sending the correct calls. # Check that we are sending the correct calls.
# Ensures that we are decoding the file content recieved from AWS. # Ensures that we are decoding the file content recieved from AWS.
expected_call = [call.get_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key="current-federal.csv")] expected_call = [call.get_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key="current-federal.csv")]
mock_client_instance.assert_has_calls(expected_call) mock_client_instance.assert_has_calls(expected_call)
# Check that the response has status code 200 # Check that the response has status code 200
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# Check that the response contains what we expect # Check that the response contains what we expect
expected_file_content = ( expected_file_content = (
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n" "Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
"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,,,," "ddomain3.gov,Federal,Armed Forces Retirement Home,,,,"
).encode() ).encode()
self.assertEqual(expected_file_content, response.content) self.assertEqual(expected_file_content, response.content)
@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"""
mock_client = MagicMock() with less_console_noise():
mock_client_instance = mock_client.return_value mock_client = MagicMock()
mock_client_instance = mock_client.return_value
with open("registrar/tests/data/fake_current_full.csv", "r") as file: with open("registrar/tests/data/fake_current_full.csv", "r") as file:
file_content = file.read() file_content = file.read()
# Mock a recieved file # Mock a recieved file
mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())} mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())}
with boto3_mocking.clients.handler_for("s3", mock_client): with boto3_mocking.clients.handler_for("s3", mock_client):
request = self.factory.get("/fake-path") request = self.factory.get("/fake-path")
response = get_current_full(request) response = get_current_full(request)
# Check that we are sending the correct calls. # Check that we are sending the correct calls.
# Ensures that we are decoding the file content recieved from AWS. # Ensures that we are decoding the file content recieved from AWS.
expected_call = [call.get_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key="current-full.csv")] expected_call = [call.get_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key="current-full.csv")]
mock_client_instance.assert_has_calls(expected_call) mock_client_instance.assert_has_calls(expected_call)
# Check that the response has status code 200 # Check that the response has status code 200
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# Check that the response contains what we expect # Check that the response contains what we expect
expected_file_content = ( expected_file_content = (
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n" "Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
"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"
"adomain2.gov,Interstate,,,,," "adomain2.gov,Interstate,,,,,"
).encode() ).encode()
self.assertEqual(expected_file_content, response.content) self.assertEqual(expected_file_content, response.content)
class ExportDataTest(MockEppLib): class ExportDataTest(MockEppLib):
@ -339,192 +346,170 @@ class ExportDataTest(MockEppLib):
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
self.domain_2.security_contact
# Invoke setter # Invoke setter
self.domain_2.security_contact self.domain_3.security_contact
# Create a CSV file in memory
# Invoke setter csv_file = StringIO()
self.domain_3.security_contact writer = csv.writer(csv_file)
# Define columns, sort fields, and filter condition
# Create a CSV file in memory columns = [
csv_file = StringIO() "Domain name",
writer = csv.writer(csv_file) "Domain type",
"Agency",
# Define columns, sort fields, and filter condition "Organization name",
columns = [ "City",
"Domain name", "State",
"Domain type", "AO",
"Agency", "AO email",
"Organization name", "Security contact email",
"City", "Status",
"State", "Expiration date",
"AO", ]
"AO email", sort_fields = ["domain__name"]
"Security contact email", filter_condition = {
"Status", "domain__state__in": [
"Expiration date", Domain.State.READY,
] Domain.State.DNS_NEEDED,
sort_fields = ["domain__name"] Domain.State.ON_HOLD,
filter_condition = { ],
"domain__state__in": [ }
Domain.State.READY, self.maxDiff = None
Domain.State.DNS_NEEDED, # Call the export functions
Domain.State.ON_HOLD, write_header(writer, columns)
], write_body(writer, columns, sort_fields, filter_condition)
} # Reset the CSV file's position to the beginning
csv_file.seek(0)
self.maxDiff = None # Read the content into a variable
# Call the export functions csv_content = csv_file.read()
write_header(writer, columns) # We expect READY domains,
write_body(writer, columns, sort_fields, filter_condition) # sorted alphabetially by domain name
expected_content = (
# Reset the CSV file's position to the beginning "Domain name,Domain type,Agency,Organization name,City,State,AO,"
csv_file.seek(0) "AO email,Security contact email,Status,Expiration date\n"
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n"
# Read the content into a variable "adomain2.gov,Interstate,(blank),Dns needed\n"
csv_content = csv_file.read() "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"
# We expect READY domains, )
# sorted alphabetially by domain name # Normalize line endings and remove commas,
expected_content = ( # spaces and leading/trailing whitespace
"Domain name,Domain type,Agency,Organization name,City,State,AO," csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
"AO email,Security contact email,Status,Expiration date\n" expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n" self.assertEqual(csv_content, expected_content)
"adomain2.gov,Interstate,(blank),Dns needed\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"
)
# 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)
def test_write_body(self): def test_write_body(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"""
# Create a CSV file in memory with less_console_noise():
csv_file = StringIO() # Create a CSV file in memory
writer = csv.writer(csv_file) csv_file = StringIO()
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",
"Domain type", "Domain type",
"Agency", "Agency",
"Organization name", "Organization name",
"City", "City",
"State", "State",
"AO", "AO",
"AO email", "AO email",
"Submitter", "Submitter",
"Submitter title", "Submitter title",
"Submitter email", "Submitter email",
"Submitter phone", "Submitter phone",
"Security contact email", "Security contact email",
"Status", "Status",
] ]
sort_fields = ["domain__name"] sort_fields = ["domain__name"]
filter_condition = { filter_condition = {
"domain__state__in": [ "domain__state__in": [
Domain.State.READY, Domain.State.READY,
Domain.State.DNS_NEEDED, Domain.State.DNS_NEEDED,
Domain.State.ON_HOLD, Domain.State.ON_HOLD,
], ],
} }
# Call the export functions
# Call the export functions write_header(writer, columns)
write_header(writer, columns) write_body(writer, columns, sort_fields, filter_condition)
write_body(writer, columns, sort_fields, filter_condition) # Reset the CSV file's position to the beginning
csv_file.seek(0)
# Reset the CSV file's position to the beginning # Read the content into a variable
csv_file.seek(0) csv_content = csv_file.read()
# We expect READY domains,
# Read the content into a variable # sorted alphabetially by domain name
csv_content = csv_file.read() expected_content = (
"Domain name,Domain type,Agency,Organization name,City,State,AO,"
# We expect READY domains, "AO email,Submitter,Submitter title,Submitter email,Submitter phone,"
# sorted alphabetially by domain name "Security contact email,Status\n"
expected_content = ( "adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n"
"Domain name,Domain type,Agency,Organization name,City,State,AO," "adomain2.gov,Interstate,Dns needed\n"
"AO email,Submitter,Submitter title,Submitter email,Submitter phone," "cdomain1.gov,Federal - Executive,World War I Centennial Commission,Ready\n"
"Security contact email,Status\n" "ddomain3.gov,Federal,Armed Forces Retirement Home,On hold\n"
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n" )
"adomain2.gov,Interstate,Dns needed\n" # Normalize line endings and remove commas,
"cdomain1.gov,Federal - Executive,World War I Centennial Commission,Ready\n" # spaces and leading/trailing whitespace
"ddomain3.gov,Federal,Armed Forces Retirement Home,On hold\n" 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)
# 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)
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"""
# Create a CSV file in memory with less_console_noise():
csv_file = StringIO() # Create a CSV file in memory
writer = csv.writer(csv_file) csv_file = StringIO()
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",
"Domain type", "Domain type",
"Agency", "Agency",
"Organization name", "Organization name",
"City", "City",
"State", "State",
"Security contact email", "Security contact email",
] ]
sort_fields = ["domain__name", "federal_agency", "organization_type"] sort_fields = ["domain__name", "federal_agency", "organization_type"]
filter_condition = { filter_condition = {
"organization_type__icontains": "federal", "organization_type__icontains": "federal",
"domain__state__in": [ "domain__state__in": [
Domain.State.READY, Domain.State.READY,
Domain.State.DNS_NEEDED, Domain.State.DNS_NEEDED,
Domain.State.ON_HOLD, Domain.State.ON_HOLD,
], ],
} }
# Call the export functions
# Call the export functions write_header(writer, columns)
write_header(writer, columns) write_body(writer, columns, sort_fields, filter_condition)
write_body(writer, columns, sort_fields, filter_condition) # Reset the CSV file's position to the beginning
csv_file.seek(0)
# Reset the CSV file's position to the beginning # Read the content into a variable
csv_file.seek(0) csv_content = csv_file.read()
# We expect READY domains,
# Read the content into a variable # federal only
csv_content = csv_file.read() # sorted alphabetially by domain name
expected_content = (
# We expect READY domains, "Domain name,Domain type,Agency,Organization name,City,"
# federal only "State,Security contact email\n"
# sorted alphabetially by domain name "adomain10.gov,Federal,Armed Forces Retirement Home\n"
expected_content = ( "cdomain1.gov,Federal - Executive,World War I Centennial Commission\n"
"Domain name,Domain type,Agency,Organization name,City," "ddomain3.gov,Federal,Armed Forces Retirement Home\n"
"State,Security contact email\n" )
"adomain10.gov,Federal,Armed Forces Retirement Home\n" # Normalize line endings and remove commas,
"cdomain1.gov,Federal - Executive,World War I Centennial Commission\n" # spaces and leading/trailing whitespace
"ddomain3.gov,Federal,Armed Forces Retirement Home\n" 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)
# 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)
def test_write_body_with_date_filter_pulls_domains_in_range(self): def test_write_body_with_date_filter_pulls_domains_in_range(self):
"""Test that domains that are """Test that domains that are
@ -538,88 +523,88 @@ 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
csv_file = StringIO()
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()) and a specific time (using 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()))
# Create a CSV file in memory # Define columns, sort fields, and filter condition
csv_file = StringIO() columns = [
writer = csv.writer(csv_file) "Domain name",
# We use timezone.make_aware to sync to server time a datetime object with the current date (using date.today()) "Domain type",
# and a specific time (using datetime.min.time()). "Agency",
end_date = timezone.make_aware(datetime.combine(date.today() + timedelta(days=2), datetime.min.time())) "Organization name",
start_date = timezone.make_aware(datetime.combine(date.today() - timedelta(days=2), datetime.min.time())) "City",
"State",
"Status",
"Expiration date",
]
sort_fields = [
"created_at",
"domain__name",
]
sort_fields_for_deleted_domains = [
"domain__deleted",
"domain__name",
]
filter_condition = {
"domain__state__in": [
Domain.State.READY,
],
"domain__first_ready__lte": end_date,
"domain__first_ready__gte": start_date,
}
filter_conditions_for_deleted_domains = {
"domain__state__in": [
Domain.State.DELETED,
],
"domain__deleted__lte": end_date,
"domain__deleted__gte": start_date,
}
# Define columns, sort fields, and filter condition # Call the export functions
columns = [ write_header(writer, columns)
"Domain name", write_body(
"Domain type", writer,
"Agency", columns,
"Organization name", sort_fields,
"City", filter_condition,
"State", )
"Status", write_body(
"Expiration date", writer,
] columns,
sort_fields = [ sort_fields_for_deleted_domains,
"created_at", filter_conditions_for_deleted_domains,
"domain__name", )
]
sort_fields_for_deleted_domains = [
"domain__deleted",
"domain__name",
]
filter_condition = {
"domain__state__in": [
Domain.State.READY,
],
"domain__first_ready__lte": end_date,
"domain__first_ready__gte": start_date,
}
filter_conditions_for_deleted_domains = {
"domain__state__in": [
Domain.State.DELETED,
],
"domain__deleted__lte": end_date,
"domain__deleted__gte": start_date,
}
# Call the export functions # Reset the CSV file's position to the beginning
write_header(writer, columns) csv_file.seek(0)
write_body(
writer,
columns,
sort_fields,
filter_condition,
)
write_body(
writer,
columns,
sort_fields_for_deleted_domains,
filter_conditions_for_deleted_domains,
)
# Reset the CSV file's position to the beginning # Read the content into a variable
csv_file.seek(0) csv_content = csv_file.read()
# Read the content into a variable # We expect READY domains first, created between today-2 and today+2, sorted by created_at then name
csv_content = csv_file.read() # and DELETED domains deleted between today-2 and today+2, sorted by deleted then name
expected_content = (
"Domain name,Domain type,Agency,Organization name,City,"
"State,Status,Expiration date\n"
"cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,\n"
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,\n"
"zdomain9.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
"sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
"xdomain7.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
)
# We expect READY domains first, created between today-2 and today+2, sorted by created_at then name # Normalize line endings and remove commas,
# and DELETED domains deleted between today-2 and today+2, sorted by deleted then name # spaces and leading/trailing whitespace
expected_content = ( csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
"Domain name,Domain type,Agency,Organization name,City," expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
"State,Status,Expiration date\n"
"cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,\n"
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,\n"
"zdomain9.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
"sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
"xdomain7.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
)
# Normalize line endings and remove commas, self.assertEqual(csv_content, expected_content)
# 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):

File diff suppressed because it is too large Load diff

View file

@ -23,6 +23,7 @@ SAMPLE_KWARGS = {
"content_type_id": "2", "content_type_id": "2",
"object_id": "3", "object_id": "3",
"domain": "whitehouse.gov", "domain": "whitehouse.gov",
"user_pk": "1",
} }
# Our test suite will ignore some namespaces. # Our test suite will ignore some namespaces.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

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__)
@ -70,7 +70,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)"

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

@ -12,6 +12,7 @@ from .domain import (
DomainUsersView, DomainUsersView,
DomainAddUserView, DomainAddUserView,
DomainInvitationDeleteView, DomainInvitationDeleteView,
DomainDeleteUserView,
) )
from .health import * from .health import *
from .index import * from .index import *

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,
@ -33,6 +34,7 @@ from registrar.utility.errors import (
SecurityEmailErrorCodes, SecurityEmailErrorCodes,
) )
from registrar.models.utility.contact_error import ContactError from registrar.models.utility.contact_error import ContactError
from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView
from ..forms import ( from ..forms import (
ContactForm, ContactForm,
@ -141,11 +143,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
@ -569,7 +572,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
@ -630,6 +633,55 @@ class DomainUsersView(DomainBaseView):
template_name = "domain_users.html" template_name = "domain_users.html"
def get_context_data(self, **kwargs):
"""The initial value for the form (which is a formset here)."""
context = super().get_context_data(**kwargs)
# Add conditionals to the context (such as "can_delete_users")
context = self._add_booleans_to_context(context)
# Add modal buttons to the context (such as for delete)
context = self._add_modal_buttons_to_context(context)
# Get the email of the current user
context["current_user_email"] = self.request.user.email
return context
def _add_booleans_to_context(self, context):
# Determine if the current user can delete managers
domain_pk = None
can_delete_users = False
if self.kwargs is not None and "pk" in self.kwargs:
domain_pk = self.kwargs["pk"]
# Prevent the end user from deleting themselves as a manager if they are the
# only manager that exists on a domain.
can_delete_users = UserDomainRole.objects.filter(domain__id=domain_pk).count() > 1
context["can_delete_users"] = can_delete_users
return context
def _add_modal_buttons_to_context(self, context):
"""Adds modal buttons (and their HTML) to the context"""
# Create HTML for the modal button
modal_button = (
'<button type="submit" '
'class="usa-button usa-button--secondary" '
'name="delete_domain_manager">Yes, remove domain manager</button>'
)
context["modal_button"] = modal_button
# Create HTML for the modal button when deleting yourself
modal_button_self = (
'<button type="submit" '
'class="usa-button usa-button--secondary" '
'name="delete_domain_manager_self">Yes, remove myself</button>'
)
context["modal_button_self"] = modal_button_self
return context
class DomainAddUserView(DomainFormBaseView): class DomainAddUserView(DomainFormBaseView):
"""Inside of a domain's user management, a form for adding users. """Inside of a domain's user management, a form for adding users.
@ -743,3 +795,60 @@ class DomainInvitationDeleteView(DomainInvitationPermissionDeleteView, SuccessMe
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"Successfully canceled invitation for {self.object.email}."
class DomainDeleteUserView(UserDomainRolePermissionDeleteView):
"""Inside of a domain's user management, a form for deleting users."""
object: UserDomainRole # workaround for type mismatch in DeleteView
def get_object(self, queryset=None):
"""Custom get_object definition to grab a UserDomainRole object from a domain_id and user_id"""
domain_id = self.kwargs.get("pk")
user_id = self.kwargs.get("user_pk")
return UserDomainRole.objects.get(domain=domain_id, user=user_id)
def get_success_url(self):
"""Refreshes the page after a delete is successful"""
return reverse("domain-users", kwargs={"pk": self.object.domain.id})
def get_success_message(self, delete_self=False):
"""Returns confirmation content for the deletion event"""
# Grab the text representation of the user we want to delete
email_or_name = self.object.user.email
if email_or_name is None or email_or_name.strip() == "":
email_or_name = self.object.user
# If the user is deleting themselves, return a specific message.
# If not, return something more generic.
if delete_self:
message = f"You are no longer managing the domain {self.object.domain}."
else:
message = f"Removed {email_or_name} as a manager for this domain."
return message
def form_valid(self, form):
"""Delete the specified user on this domain."""
# Delete the object
super().form_valid(form)
# Is the user deleting themselves? If so, display a different message
delete_self = self.request.user == self.object.user
# Add a success message
messages.success(self.request, self.get_success_message(delete_self))
return redirect(self.get_success_url())
def post(self, request, *args, **kwargs):
"""Custom post implementation to redirect to home in the event that the user deletes themselves"""
response = super().post(request, *args, **kwargs)
# If the user is deleting themselves, redirect to home
delete_self = self.request.user == self.object.user
if delete_self:
return redirect(reverse("home"))
return response

View file

@ -286,6 +286,43 @@ class DomainApplicationPermission(PermissionsLoginMixin):
return True return True
class UserDeleteDomainRolePermission(PermissionsLoginMixin):
"""Permission mixin for UserDomainRole if user
has access, otherwise 403"""
def has_permission(self):
"""Check if this user has access to this domain application.
The user is in self.request.user and the domain needs to be looked
up from the domain's primary key in self.kwargs["pk"]
"""
domain_pk = self.kwargs["pk"]
user_pk = self.kwargs["user_pk"]
# Check if the user is authenticated
if not self.request.user.is_authenticated:
return False
# Check if the UserDomainRole object exists, then check
# if the user requesting the delete has permissions to do so
has_delete_permission = UserDomainRole.objects.filter(
user=user_pk,
domain=domain_pk,
domain__permissions__user=self.request.user,
).exists()
if not has_delete_permission:
return False
# Check if more than one manager exists on the domain.
# If only one exists, prevent this from happening
has_multiple_managers = len(UserDomainRole.objects.filter(domain=domain_pk)) > 1
if not has_multiple_managers:
return False
return True
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

View file

@ -4,6 +4,7 @@ import abc # abstract base class
from django.views.generic import DetailView, DeleteView, TemplateView from django.views.generic import DetailView, DeleteView, TemplateView
from registrar.models import Domain, DomainApplication, DomainInvitation from registrar.models import Domain, DomainApplication, DomainInvitation
from registrar.models.user_domain_role import UserDomainRole
from .mixins import ( from .mixins import (
DomainPermission, DomainPermission,
@ -11,6 +12,7 @@ from .mixins import (
DomainApplicationPermissionWithdraw, DomainApplicationPermissionWithdraw,
DomainInvitationPermission, DomainInvitationPermission,
ApplicationWizardPermission, ApplicationWizardPermission,
UserDeleteDomainRolePermission,
) )
import logging import logging
@ -130,3 +132,20 @@ class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteV
model = DomainApplication model = DomainApplication
object: DomainApplication object: DomainApplication
class UserDomainRolePermissionDeleteView(UserDeleteDomainRolePermission, DeleteView, abc.ABC):
"""Abstract base view for deleting a UserDomainRole.
This abstract view cannot be instantiated. Actual views must specify
`template_name`.
"""
# DetailView property for what model this is viewing
model = UserDomainRole
# workaround for type mismatch in DeleteView
object: UserDomainRole
# variable name in template context for the model object
context_object_name = "userdomainrole"