diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index f65007b2b..dec0b9fac 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -48,7 +48,7 @@ All other changes require just a single approving review.--> - [ ] Added at least 2 developers as PR reviewers (only 1 will need to approve) - [ ] Messaged on Slack or in standup to notify the team that a PR is ready for review - [ ] Changes to “how we do things” are documented in READMEs and or onboarding guide -- [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the assoicated migrations file has been commited. +- [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the associated migrations file has been commited. #### Ensured code standards are met (Original Developer) @@ -72,7 +72,7 @@ All other changes require just a single approving review.--> - [ ] Reviewed this code and left comments - [ ] Checked that all code is adequately covered by tests - [ ] Made it clear which comments need to be addressed before this work is merged -- [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the assoicated migrations file has been commited. +- [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the associated migrations file has been commited. #### Ensured code standards are met (Code reviewer) diff --git a/.github/workflows/deploy-development.yaml b/.github/workflows/deploy-development.yaml index 562b2b11f..686635c20 100644 --- a/.github/workflows/deploy-development.yaml +++ b/.github/workflows/deploy-development.yaml @@ -38,3 +38,11 @@ jobs: cf_org: cisa-dotgov cf_space: development push_arguments: "-f ops/manifests/manifest-development.yaml" + - name: Run Django migrations + uses: cloud-gov/cg-cli-tools@main + with: + cf_username: ${{ secrets.CF_DEVELOPMENT_USERNAME }} + cf_password: ${{ secrets.CF_DEVELOPMENT_PASSWORD }} + cf_org: cisa-dotgov + cf_space: development + cf_command: "run-task getgov-development --command 'python manage.py migrate' --name migrate" diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml index 8523af013..2033ee51c 100644 --- a/.github/workflows/migrate.yaml +++ b/.github/workflows/migrate.yaml @@ -26,7 +26,6 @@ on: - rb - ko - ab - - bl - rjm - dk diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml index 3848a33bd..f8730c865 100644 --- a/.github/workflows/reset-db.yaml +++ b/.github/workflows/reset-db.yaml @@ -26,7 +26,6 @@ on: - rb - ko - ab - - bl - rjm - dk diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6d34f80b5..fd9e31b91 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -36,7 +36,7 @@ jobs: - name: Unit tests 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: runs-on: ubuntu-latest diff --git a/docs/developer/README.md b/docs/developer/README.md index 57985d6e2..dc4c9ddd2 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -145,7 +145,7 @@ You can change the logging verbosity, if needed. Do a web search for "django log ## 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. diff --git a/docs/operations/README.md b/docs/operations/README.md index 0e7b7a432..0bd55ab51 100644 --- a/docs/operations/README.md +++ b/docs/operations/README.md @@ -237,3 +237,7 @@ Bugs on production software need to be documented quickly and triaged to determi 3. In the case where the engineering lead is is unresponsive or unavailable to assign the ticket immediately, the product team will make sure an engineer volunteers or is assigned to the ticket/PR review ASAP. 4. Once done, the developer must make a PR and should tag the assigned PR reviewers in our Slack dev channel stating that the PR is now waiting on their review. These reviewers should drop other tasks in order to review this promptly. 5. See the the section above on [Making bug fixes on stable](#making-bug-fixes-on-stable-during-production) for how to push changes to stable once the PR is approved + +# Investigating and monitoring the health of the Registrar + +Sometimes, we may want individuals to routinely monitor the Registrar's health, such as after big feature launches. The cadence of such monitoring and what we look for is subject to change and is instead documented in [Checklist for production verification document](https://docs.google.com/document/d/15b_qwEZMiL76BHeRHnznV1HxDQcxNRt--vPSEfixBOI). All project team members should feel free to suggest edits to this document and should refer to it if production-level monitoring is underway. \ No newline at end of file diff --git a/ops/manifests/manifest-bl.yaml b/ops/manifests/manifest-bl.yaml deleted file mode 100644 index ea0617427..000000000 --- a/ops/manifests/manifest-bl.yaml +++ /dev/null @@ -1,32 +0,0 @@ ---- -applications: -- name: getgov-bl - buildpacks: - - python_buildpack - path: ../../src - instances: 1 - memory: 512M - stack: cflinuxfs4 - timeout: 180 - command: ./run.sh - health-check-type: http - health-check-http-endpoint: /health - health-check-invocation-timeout: 40 - env: - # Send stdout and stderr straight to the terminal without buffering - PYTHONUNBUFFERED: yup - # Tell Django where to find its configuration - DJANGO_SETTINGS_MODULE: registrar.config.settings - # Tell Django where it is being hosted - DJANGO_BASE_URL: https://getgov-bl.app.cloud.gov - # Tell Django how much stuff to log - DJANGO_LOG_LEVEL: INFO - # default public site location - GETGOV_PUBLIC_SITE_URL: https://get.gov - # Flag to disable/enable features in prod environments - IS_PRODUCTION: False - routes: - - route: getgov-bl.app.cloud.gov - services: - - getgov-credentials - - getgov-bl-database diff --git a/src/djangooidc/tests/test_views.py b/src/djangooidc/tests/test_views.py index 63b23df96..4193f723b 100644 --- a/src/djangooidc/tests/test_views.py +++ b/src/djangooidc/tests/test_views.py @@ -35,155 +35,155 @@ class ViewsTest(TestCase): pass def test_openid_sets_next(self, mock_client): - # setup - callback_url = reverse("openid_login_callback") - # mock - mock_client.create_authn_request.side_effect = self.say_hi - mock_client.get_default_acr_value.side_effect = self.create_acr - # test - response = self.client.get(reverse("login"), {"next": callback_url}) - # assert - session = mock_client.create_authn_request.call_args[0][0] - self.assertEqual(session["next"], callback_url) - self.assertEqual(response.status_code, 200) - self.assertContains(response, "Hi") + with less_console_noise(): + # setup + callback_url = reverse("openid_login_callback") + # mock + mock_client.create_authn_request.side_effect = self.say_hi + mock_client.get_default_acr_value.side_effect = self.create_acr + # test + response = self.client.get(reverse("login"), {"next": callback_url}) + # assert + session = mock_client.create_authn_request.call_args[0][0] + self.assertEqual(session["next"], callback_url) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Hi") def test_openid_raises(self, mock_client): - # mock - mock_client.create_authn_request.side_effect = Exception("Test") - # test with less_console_noise(): + # mock + mock_client.create_authn_request.side_effect = Exception("Test") + # test response = self.client.get(reverse("login")) - # assert - self.assertEqual(response.status_code, 500) - self.assertTemplateUsed(response, "500.html") - self.assertIn("Server error", response.content.decode("utf-8")) + # assert + self.assertEqual(response.status_code, 500) + self.assertTemplateUsed(response, "500.html") + self.assertIn("Server error", response.content.decode("utf-8")) 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), 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(): + # 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")) - # assert - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, "/") + # assert + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/") def test_login_callback_reads_next(self, mock_client): - # setup - session = self.client.session - session["next"] = reverse("logout") - session.save() - # mock - mock_client.callback.side_effect = self.user_info - # test - with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): - response = self.client.get(reverse("openid_login_callback")) - # assert - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, reverse("logout")) + with less_console_noise(): + # setup + session = self.client.session + session["next"] = reverse("logout") + session.save() + # mock + mock_client.callback.side_effect = self.user_info + # test + with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): + response = self.client.get(reverse("openid_login_callback")) + # assert + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, reverse("logout")) def test_login_callback_no_step_up_auth(self, mock_client): """Walk through login_callback when requires_step_up_auth returns False and assert that we have a redirect to /""" - # setup - session = self.client.session - session.save() - # mock - mock_client.callback.side_effect = self.user_info - # test - with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): - response = self.client.get(reverse("openid_login_callback")) - # assert - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, "/") + with less_console_noise(): + # setup + session = self.client.session + session.save() + # mock + mock_client.callback.side_effect = self.user_info + # test + with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): + response = self.client.get(reverse("openid_login_callback")) + # assert + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, "/") def test_requires_step_up_auth(self, mock_client): """Invoke login_callback passing it a request when requires_step_up_auth returns True and assert that session is updated and create_authn_request (mock) is called.""" - # Configure the mock to return an expected value for get_step_up_acr_value - mock_client.return_value.get_step_up_acr_value.return_value = "step_up_acr_value" - - # Create a mock request - request = self.factory.get("/some-url") - request.session = {"acr_value": ""} - - # Ensure that the CLIENT instance used in login_callback is the mock - # patch requires_step_up_auth to return True - with patch("djangooidc.views.requires_step_up_auth", return_value=True), patch( - "djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock() - ) as mock_create_authn_request: - login_callback(request) - - # create_authn_request only gets called when requires_step_up_auth is True - # and it changes this acr_value in request.session - - # Assert that acr_value is no longer empty string - self.assertNotEqual(request.session["acr_value"], "") - # And create_authn_request was called again - mock_create_authn_request.assert_called_once() + with less_console_noise(): + # Configure the mock to return an expected value for get_step_up_acr_value + mock_client.return_value.get_step_up_acr_value.return_value = "step_up_acr_value" + # Create a mock request + request = self.factory.get("/some-url") + request.session = {"acr_value": ""} + # Ensure that the CLIENT instance used in login_callback is the mock + # patch requires_step_up_auth to return True + with patch("djangooidc.views.requires_step_up_auth", return_value=True), patch( + "djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock() + ) as mock_create_authn_request: + login_callback(request) + # create_authn_request only gets called when requires_step_up_auth is True + # and it changes this acr_value in request.session + # Assert that acr_value is no longer empty string + self.assertNotEqual(request.session["acr_value"], "") + # And create_authn_request was called again + mock_create_authn_request.assert_called_once() def test_does_not_requires_step_up_auth(self, mock_client): """Invoke login_callback passing it a request when requires_step_up_auth returns False and assert that session is not updated and create_authn_request (mock) is not called. Possibly redundant with test_login_callback_requires_step_up_auth""" - # Create a mock request - request = self.factory.get("/some-url") - request.session = {"acr_value": ""} - - # Ensure that the CLIENT instance used in login_callback is the mock - # patch requires_step_up_auth to return False - with patch("djangooidc.views.requires_step_up_auth", return_value=False), patch( - "djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock() - ) as mock_create_authn_request: - 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 - - # Assert that acr_value is NOT updated by testing that it is still an empty string - self.assertEqual(request.session["acr_value"], "") - # Assert create_authn_request was not called - mock_create_authn_request.assert_not_called() + with less_console_noise(): + # Create a mock request + request = self.factory.get("/some-url") + request.session = {"acr_value": ""} + # Ensure that the CLIENT instance used in login_callback is the mock + # patch requires_step_up_auth to return False + with patch("djangooidc.views.requires_step_up_auth", return_value=False), patch( + "djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock() + ) as mock_create_authn_request: + login_callback(request) + # create_authn_request only gets called when requires_step_up_auth is True + # 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 create_authn_request was not called + mock_create_authn_request.assert_not_called() @patch("djangooidc.views.authenticate") def test_login_callback_raises(self, mock_auth, mock_client): - # mock - mock_client.callback.side_effect = self.user_info - mock_auth.return_value = None - # test - with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): - response = self.client.get(reverse("openid_login_callback")) - # assert - self.assertEqual(response.status_code, 401) - self.assertTemplateUsed(response, "401.html") - self.assertIn("Unauthorized", response.content.decode("utf-8")) + with less_console_noise(): + # mock + mock_client.callback.side_effect = self.user_info + mock_auth.return_value = None + # test + with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise(): + response = self.client.get(reverse("openid_login_callback")) + # assert + self.assertEqual(response.status_code, 401) + self.assertTemplateUsed(response, "401.html") + self.assertIn("Unauthorized", response.content.decode("utf-8")) 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(): - 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) + # 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(): + 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") def test_logout_always_logs_out(self, mock_logout, _): @@ -194,12 +194,13 @@ class ViewsTest(TestCase): self.assertTrue(mock_logout.called) def test_logout_callback_redirects(self, _): - # setup - session = self.client.session - session["next"] = reverse("logout") - session.save() - # test - response = self.client.get(reverse("openid_logout_callback")) - # assert - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, reverse("logout")) + with less_console_noise(): + # setup + session = self.client.session + session["next"] = reverse("logout") + session.save() + # test + response = self.client.get(reverse("openid_logout_callback")) + # assert + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, reverse("logout")) diff --git a/src/docker-compose.yml b/src/docker-compose.yml index ba6530674..fdf069f56 100644 --- a/src/docker-compose.yml +++ b/src/docker-compose.yml @@ -25,6 +25,8 @@ services: - DJANGO_SECRET_KEY=really-long-random-string-BNPecI7+s8jMahQcGHZ3XQ5yUfRrSibdapVLIz0UemdktVPofDKcoy # Run Django in debug mode on local - DJANGO_DEBUG=True + # Set DJANGO_LOG_LEVEL in env + - DJANGO_LOG_LEVEL # Run Django without production flags - IS_PRODUCTION=False # Tell Django where it is being hosted diff --git a/src/epplibwrapper/tests/common.py b/src/epplibwrapper/tests/common.py new file mode 100644 index 000000000..122965ae8 --- /dev/null +++ b/src/epplibwrapper/tests/common.py @@ -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() diff --git a/src/epplibwrapper/tests/test_pool.py b/src/epplibwrapper/tests/test_pool.py index 1c36d26da..f8e556445 100644 --- a/src/epplibwrapper/tests/test_pool.py +++ b/src/epplibwrapper/tests/test_pool.py @@ -9,7 +9,7 @@ from epplibwrapper.socket import Socket from epplibwrapper.utility.pool import EPPConnectionPool from registrar.models.domain import registry from contextlib import ExitStack - +from .common import less_console_noise import logging try: @@ -135,23 +135,26 @@ class TestConnectionPool(TestCase): 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, "receive", fake_receive)) - # Restart the connection pool - registry.start_connection_pool() - # Pool should be running, and be the right size - self.assertEqual(registry.pool_status.connection_success, True) - self.assertEqual(registry.pool_status.pool_running, True) + with less_console_noise(): + # Restart the connection pool + registry.start_connection_pool() + # Pool should be running, and be the right size + self.assertEqual(registry.pool_status.connection_success, True) + self.assertEqual(registry.pool_status.pool_running, True) - # Send a command - result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) + # Send a command + result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) - # 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) + # 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) - # 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"]) + # 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"]) + # Kill the connection pool + registry.kill_pool() @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success) def test_pool_restarts_on_send(self): @@ -198,35 +201,43 @@ class TestConnectionPool(TestCase): xml = (location).read_bytes() return xml + def do_nothing(command): + pass + # Mock what happens inside the "with" with ExitStack() as stack: stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket)) stack.enter_context(patch.object(Socket, "connect", self.fake_client)) + 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, "receive", fake_receive)) - # Kill the connection pool - registry.kill_pool() + with less_console_noise(): + # 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 - # that they cannot connect to EPP - with self.assertRaises(RegistryError): - expected = "InfoDomain failed to execute due to a connection error." + # An exception should be raised as end user will be informed + # that they cannot connect to EPP + with self.assertRaises(RegistryError): + expected = "InfoDomain failed to execute due to a connection error." + result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) + self.assertEqual(result, expected) + + # A subsequent command should be successful, as the pool restarts 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 - result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) - # 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) - - # 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"]) + # 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"]) + # Kill the connection pool + registry.kill_pool() @patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success) def test_raises_connection_error(self): @@ -236,13 +247,16 @@ class TestConnectionPool(TestCase): with ExitStack() as stack: stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket)) stack.enter_context(patch.object(Socket, "connect", self.fake_client)) + with less_console_noise(): + # Start the connection pool + registry.start_connection_pool() - # Pool should be running - self.assertEqual(registry.pool_status.connection_success, True) - self.assertEqual(registry.pool_status.pool_running, True) + # Pool should be running + self.assertEqual(registry.pool_status.connection_success, True) + self.assertEqual(registry.pool_status.pool_running, True) - # Try to send a command out - should fail - with self.assertRaises(RegistryError): - expected = "InfoDomain failed to execute due to a connection error." - result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) - self.assertEqual(result, expected) + # Try to send a command out - should fail + with self.assertRaises(RegistryError): + expected = "InfoDomain failed to execute due to a connection error." + result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True) + self.assertEqual(result, expected) diff --git a/src/epplibwrapper/utility/pool.py b/src/epplibwrapper/utility/pool.py index 93edb2782..4f54e14ce 100644 --- a/src/epplibwrapper/utility/pool.py +++ b/src/epplibwrapper/utility/pool.py @@ -85,6 +85,21 @@ class EPPConnectionPool(ConnectionPool): logger.error(message, exc_info=True) 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: """Creates and returns a socket instance""" socket = Socket(client, login) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index bd5555805..4034bf35b 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -20,6 +20,8 @@ from . import models from auditlog.models import LogEntry # type: ignore from auditlog.admin import LogEntryAdmin # type: ignore from django_fsm import TransitionNotAllowed # type: ignore +from django.utils.safestring import mark_safe +from django.utils.html import escape logger = logging.getLogger(__name__) @@ -452,6 +454,60 @@ class ContactAdmin(ListHeaderAdmin): readonly_fields.extend([field for field in self.analyst_readonly_fields]) return readonly_fields # Read-only fields for analysts + def change_view(self, request, object_id, form_url="", extra_context=None): + """Extend the change_view for Contact objects in django admin. + Customize to display related objects to the Contact. These will be passed + through the messages construct to the template for display to the user.""" + + # Fetch the Contact instance + contact = models.Contact.objects.get(pk=object_id) + + # initialize related_objects array + related_objects = [] + # for all defined fields in the model + for related_field in contact._meta.get_fields(): + # if the field is a relation to another object + if related_field.is_relation: + # Check if the related field is not None + related_manager = getattr(contact, related_field.name) + if related_manager is not None: + # Check if it's a ManyToManyField/reverse ForeignKey or a OneToOneField + # Do this by checking for get_queryset method on the related_manager + if hasattr(related_manager, "get_queryset"): + # Handles ManyToManyRel and ManyToOneRel + queryset = related_manager.get_queryset() + else: + # Handles OneToOne rels, ie. User + queryset = [related_manager] + + for obj in queryset: + # for each object, build the edit url in this view and add as tuple + # to the related_objects array + app_label = obj._meta.app_label + model_name = obj._meta.model_name + obj_id = obj.id + change_url = reverse("admin:%s_%s_change" % (app_label, model_name), args=[obj_id]) + related_objects.append((change_url, obj)) + + if related_objects: + message = "" + if len(related_objects) > 5: + related_objects_over_five = len(related_objects) - 5 + message += f"

And {related_objects_over_five} more...

" + + message_html = mark_safe(message) # nosec + messages.warning( + request, + message_html, + ) + + return super().change_view(request, object_id, form_url, extra_context=extra_context) + class WebsiteAdmin(ListHeaderAdmin): """Custom website admin class.""" @@ -570,7 +626,7 @@ class DomainInformationAdmin(ListHeaderAdmin): search_help_text = "Search by domain." fieldsets = [ - (None, {"fields": ["creator", "domain_application"]}), + (None, {"fields": ["creator", "domain_application", "notes"]}), ( "Type of organization", { @@ -621,6 +677,7 @@ class DomainInformationAdmin(ListHeaderAdmin): "type_of_work", "more_organization_information", "domain", + "domain_application", "submitter", "no_other_contacts_rationale", "anything_else", @@ -737,7 +794,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): # Detail view form = DomainApplicationAdminForm fieldsets = [ - (None, {"fields": ["status", "investigator", "creator", "approved_domain"]}), + (None, {"fields": ["status", "investigator", "creator", "approved_domain", "notes"]}), ( "Type of organization", { @@ -789,6 +846,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): "creator", "about_your_organization", "requested_domain", + "approved_domain", "alternative_domains", "purpose", "submitter", @@ -826,14 +884,11 @@ class DomainApplicationAdmin(ListHeaderAdmin): if ( obj and original_obj.status == models.DomainApplication.ApplicationStatus.APPROVED - and ( - obj.status == models.DomainApplication.ApplicationStatus.REJECTED - or obj.status == models.DomainApplication.ApplicationStatus.INELIGIBLE - ) + and obj.status != models.DomainApplication.ApplicationStatus.APPROVED and not obj.domain_is_not_active() ): # If an admin tried to set an approved application to - # rejected or ineligible and the related domain is already + # another status and the related domain is already # active, shortcut the action and throw a friendly # error message. This action would still not go through # shortcut or not as the rules are duplicated on the model, @@ -991,6 +1046,13 @@ class DomainAdmin(ListHeaderAdmin): "deleted", ] + fieldsets = ( + ( + None, + {"fields": ["name", "state", "expiration_date", "first_ready", "deleted"]}, + ), + ) + # this ordering effects the ordering of results # in autocomplete_fields for domain ordering = ["name"] @@ -1239,7 +1301,7 @@ class DraftDomainAdmin(ListHeaderAdmin): search_help_text = "Search by draft domain name." -class VeryImportantPersonAdmin(ListHeaderAdmin): +class VerifiedByStaffAdmin(ListHeaderAdmin): list_display = ("email", "requestor", "truncated_notes", "created_at") search_fields = ["email"] search_help_text = "Search by email." @@ -1282,4 +1344,4 @@ admin.site.register(models.Website, WebsiteAdmin) admin.site.register(models.PublicContact, AuditedAdmin) admin.site.register(models.DomainApplication, DomainApplicationAdmin) admin.site.register(models.TransitionDomain, TransitionDomainAdmin) -admin.site.register(models.VeryImportantPerson, VeryImportantPersonAdmin) +admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index cdbbc83ee..866c7bd7d 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -283,19 +283,20 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, (function (){ // 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 let startDateInput =document.getElementById('start'); - startDateInput.value = currentDate; - + // Default the value of the end date input field to the current date let endDateInput =document.getElementById('end'); - endDateInput.value = currentDate; let exportGrowthReportButton = document.getElementById('exportLink'); if (exportGrowthReportButton) { + startDateInput.value = currentDate; + endDateInput.value = currentDate; + exportGrowthReportButton.addEventListener('click', function() { // Get the selected start and end dates let startDate = startDateInput.value; diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index de7ef6172..587b95305 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -130,7 +130,7 @@ function inlineToast(el, id, style, msg) { } } -function _checkDomainAvailability(el) { +function checkDomainAvailability(el) { const callback = (response) => { toggleInputValidity(el, (response && response.available), msg=response.message); announce(el.id, response.message); @@ -154,9 +154,6 @@ function _checkDomainAvailability(el) { fetchJSON(`available/?domain=${el.value}`, callback); } -/** Call the API to see if the domain is good. */ -const checkDomainAvailability = debounce(_checkDomainAvailability); - /** Hides the toast message and clears the aira live region. */ function clearDomainAvailability(el) { el.classList.remove('usa-input--success'); @@ -206,13 +203,33 @@ function handleInputValidation(e) { } /** On button click, handles running any associated validators. */ -function handleValidationClick(e) { +function validateFieldInput(e) { const attribute = e.target.getAttribute("validate-for") || ""; if (!attribute.length) return; const input = document.getElementById(attribute); + removeFormErrors(input, true); runValidators(input); } + +function validateFormsetInputs(e, availabilityButton) { + + // Collect input IDs from the repeatable forms + let inputs = Array.from(document.querySelectorAll('.repeatable-form input')) + + // Run validators for each input + inputs.forEach(input => { + removeFormErrors(input, true); + runValidators(input); + }); + + // Set the validate-for attribute on the button with the collected input IDs + // Not needed for functionality but nice for accessibility + inputs = inputs.map(input => input.id).join(', '); + availabilityButton.setAttribute('validate-for', inputs); + +} + // <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>> // Initialization code. @@ -232,14 +249,64 @@ function handleValidationClick(e) { for(const input of needsValidation) { input.addEventListener('input', handleInputValidation); } + const alternativeDomainsAvailability = document.getElementById('validate-alt-domains-availability'); const activatesValidation = document.querySelectorAll('[validate-for]'); + for(const button of activatesValidation) { - button.addEventListener('click', handleValidationClick); + // Adds multi-field validation for alternative domains + if (button === alternativeDomainsAvailability) { + button.addEventListener('click', (e) => { + validateFormsetInputs(e, alternativeDomainsAvailability) + }); + } else { + button.addEventListener('click', validateFieldInput); + } } })(); /** - * Delete method for formsets that diff in the view and delete in the model (Nameservers, DS Data) + * Removes form errors surrounding a form input + */ +function removeFormErrors(input, removeStaleAlerts=false){ + // Remove error message + let errorMessage = document.getElementById(`${input.id}__error-message`); + if (errorMessage) { + errorMessage.remove(); + }else{ + return + } + + // Remove error classes + if (input.classList.contains('usa-input--error')) { + input.classList.remove('usa-input--error'); + } + + // Get the form label + let label = document.querySelector(`label[for="${input.id}"]`); + if (label) { + label.classList.remove('usa-label--error'); + + // Remove error classes from parent div + let parentDiv = label.parentElement; + if (parentDiv) { + parentDiv.classList.remove('usa-form-group--error'); + } + } + + if (removeStaleAlerts){ + let staleAlerts = document.querySelectorAll(".usa-alert--error") + for (let alert of staleAlerts){ + // Don't remove the error associated with the input + if (alert.id !== `${input.id}--toast`) { + alert.remove() + } + } + } +} + +/** + * Prepare the namerservers and DS data forms delete buttons + * We will call this on the forms init, and also every time we add a form * */ function removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier){ @@ -460,6 +527,7 @@ function hideDeletedForms() { let isNameserversForm = document.querySelector(".nameservers-form"); let isOtherContactsForm = document.querySelector(".other-contacts-form"); let isDsDataForm = document.querySelector(".ds-data-form"); + let isDotgovDomain = document.querySelector(".dotgov-domain-form"); // The Nameservers formset features 2 required and 11 optionals if (isNameserversForm) { cloneIndex = 2; @@ -472,6 +540,8 @@ function hideDeletedForms() { formLabel = "Organization contact"; container = document.querySelector("#other-employees"); formIdentifier = "other_contacts" + } else if (isDotgovDomain) { + formIdentifier = "dotgov_domain" } let totalForms = document.querySelector(`#id_${formIdentifier}-TOTAL_FORMS`); @@ -554,6 +624,7 @@ function hideDeletedForms() { // Reset the values of each input to blank inputs.forEach((input) => { input.classList.remove("usa-input--error"); + input.classList.remove("usa-input--success"); if (input.type === "text" || input.type === "number" || input.type === "password" || input.type === "email" || input.type === "tel") { input.value = ""; // Set the value to an empty string @@ -566,22 +637,25 @@ function hideDeletedForms() { let selects = newForm.querySelectorAll("select"); selects.forEach((select) => { select.classList.remove("usa-input--error"); + select.classList.remove("usa-input--success"); select.selectedIndex = 0; // Set the value to an empty string }); let labels = newForm.querySelectorAll("label"); labels.forEach((label) => { label.classList.remove("usa-label--error"); + label.classList.remove("usa-label--success"); }); let usaFormGroups = newForm.querySelectorAll(".usa-form-group"); usaFormGroups.forEach((usaFormGroup) => { usaFormGroup.classList.remove("usa-form-group--error"); + usaFormGroup.classList.remove("usa-form-group--success"); }); - // Remove any existing error messages - let usaErrorMessages = newForm.querySelectorAll(".usa-error-message"); - usaErrorMessages.forEach((usaErrorMessage) => { + // Remove any existing error and success messages + let usaMessages = newForm.querySelectorAll(".usa-error-message, .usa-alert"); + usaMessages.forEach((usaErrorMessage) => { let parentDiv = usaErrorMessage.closest('div'); if (parentDiv) { parentDiv.remove(); // Remove the parent div if it exists @@ -592,7 +666,8 @@ function hideDeletedForms() { // Attach click event listener on the delete buttons of the new form let newDeleteButton = newForm.querySelector(".delete-record"); - prepareNewDeleteButton(newDeleteButton, formLabel); + if (newDeleteButton) + prepareNewDeleteButton(newDeleteButton, formLabel); // Disable the add more button if we have 13 forms if (isNameserversForm && formNum == 13) { diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index a3d631243..760c4f13a 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -143,7 +143,7 @@ h1, h2, h3, .module h3 { padding: 0; - color: var(--primary); + color: var(--link-fg); margin: units(2) 0 units(1) 0; } @@ -258,3 +258,15 @@ h1, h2, h3, #select2-id_user-results { width: 100%; } + +// Content list inside of a DjA alert, unstyled +.messagelist_content-list--unstyled { + padding-left: 0; + li { + font-family: "Source Sans Pro Web", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif; + font-size: 13.92px!important; + background: none!important; + padding: 0!important; + margin: 0!important; + } +} diff --git a/src/registrar/assets/sass/_theme/_alerts.scss b/src/registrar/assets/sass/_theme/_alerts.scss index 9ee28a357..163f243d3 100644 --- a/src/registrar/assets/sass/_theme/_alerts.scss +++ b/src/registrar/assets/sass/_theme/_alerts.scss @@ -17,5 +17,8 @@ .usa-alert__body::before { left: 1rem !important; } - } + } + .usa-alert__body.margin-left-1 { + margin-left: 0.5rem!important; + } } diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 02089ec6d..2f4121399 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -44,6 +44,22 @@ a.usa-button.disabled-link:focus { 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) { color: color('white'); } diff --git a/src/registrar/assets/sass/_theme/_lists.scss b/src/registrar/assets/sass/_theme/_lists.scss new file mode 100644 index 000000000..b97abd569 --- /dev/null +++ b/src/registrar/assets/sass/_theme/_lists.scss @@ -0,0 +1,14 @@ +@use "uswds-core" as *; + +dt { + color: color('primary-dark'); + margin-top: units(2); + font-weight: font-weight('semibold'); + // The units mixin can only get us close, so it's between + // hardcoding the value and using in markup + font-size: 16.96px; +} +dd { + margin-left: 0; +} + diff --git a/src/registrar/assets/sass/_theme/_register-form.scss b/src/registrar/assets/sass/_theme/_register-form.scss index 6d268d155..7c93f0a10 100644 --- a/src/registrar/assets/sass/_theme/_register-form.scss +++ b/src/registrar/assets/sass/_theme/_register-form.scss @@ -6,12 +6,14 @@ margin-top: units(-1); } - //Tighter spacing when H2 is immediatly after H1 +// Tighter spacing when h2 is immediatly after h1 .register-form-step .usa-fieldset:first-of-type h2:first-of-type, .register-form-step h1 + h2 { margin-top: units(1); } +// register-form-review-header is used on the summary page and +// should not be styled like the register form headers .register-form-step h3 { color: color('primary-dark'); letter-spacing: $letter-space--xs; @@ -23,6 +25,16 @@ } } +h3.register-form-review-header { + color: color('primary-dark'); + margin-top: units(2); + margin-bottom: 0; + font-weight: font-weight('semibold'); + // The units mixin can only get us close, so it's between + // hardcoding the value and using in markup + font-size: 16.96px; +} + .register-form-step h4 { margin-bottom: 0; diff --git a/src/registrar/assets/sass/_theme/_uswds-theme.scss b/src/registrar/assets/sass/_theme/_uswds-theme.scss index 0cdf6675e..a26f23508 100644 --- a/src/registrar/assets/sass/_theme/_uswds-theme.scss +++ b/src/registrar/assets/sass/_theme/_uswds-theme.scss @@ -116,6 +116,10 @@ in the form $setting: value, $theme-color-success-light: $dhs-green-30, $theme-color-success-lighter: $dhs-green-15, + /*--------------------------- + ## Emergency state + ----------------------------*/ + $theme-color-emergency: #FFC3F9, /*--------------------------- # Input settings diff --git a/src/registrar/assets/sass/_theme/styles.scss b/src/registrar/assets/sass/_theme/styles.scss index 8a2e1d2d3..0239199e7 100644 --- a/src/registrar/assets/sass/_theme/styles.scss +++ b/src/registrar/assets/sass/_theme/styles.scss @@ -10,6 +10,7 @@ --- Custom Styles ---------------------------------*/ @forward "base"; @forward "typography"; +@forward "lists"; @forward "buttons"; @forward "forms"; @forward "fieldsets"; diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index efa512f22..372434887 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -660,7 +660,6 @@ ALLOWED_HOSTS = [ "getgov-rb.app.cloud.gov", "getgov-ko.app.cloud.gov", "getgov-ab.app.cloud.gov", - "getgov-bl.app.cloud.gov", "getgov-rjm.app.cloud.gov", "getgov-dk.app.cloud.gov", "manage.get.gov", diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index ba5ae22cc..f6378b555 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -142,6 +142,11 @@ urlpatterns = [ views.DomainApplicationDeleteView.as_view(http_method_names=["post"]), name="application-delete", ), + path( + "domain//users//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 diff --git a/src/registrar/fixtures_applications.py b/src/registrar/fixtures_applications.py index 92094b876..659a3040e 100644 --- a/src/registrar/fixtures_applications.py +++ b/src/registrar/fixtures_applications.py @@ -104,7 +104,7 @@ class DomainApplicationFixture: # Random choice of agency for selects, used as placeholders for testing. else random.choice(DomainApplication.AGENCIES) # nosec ) - + da.submission_date = fake.date() da.federal_type = ( app["federal_type"] if "federal_type" in app diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 85ce28bb6..1ee7e0036 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -420,7 +420,7 @@ class AlternativeDomainForm(RegistrarForm): alternative_domain = forms.CharField( required=False, - label="", + label="Alternative domain", ) diff --git a/src/registrar/management/commands/utility/extra_transition_domain_helper.py b/src/registrar/management/commands/utility/extra_transition_domain_helper.py index 755c9b98a..c082552eb 100644 --- a/src/registrar/management/commands/utility/extra_transition_domain_helper.py +++ b/src/registrar/management/commands/utility/extra_transition_domain_helper.py @@ -182,8 +182,6 @@ class LoadExtraTransitionDomain: # STEP 5: Parse creation and expiration data updated_transition_domain = self.parse_creation_expiration_data(domain_name, transition_domain) - # Check if the instance has changed before saving - updated_transition_domain.save() updated_transition_domains.append(updated_transition_domain) logger.info(f"{TerminalColors.OKCYAN}" f"Successfully updated {domain_name}" f"{TerminalColors.ENDC}") @@ -199,6 +197,28 @@ class LoadExtraTransitionDomain: ) failed_transition_domains.append(domain_name) + updated_fields = [ + "organization_name", + "organization_type", + "federal_type", + "federal_agency", + "first_name", + "middle_name", + "last_name", + "email", + "phone", + "epp_creation_date", + "epp_expiration_date", + ] + + batch_size = 1000 + # Create a Paginator object. Bulk_update on the full dataset + # is too memory intensive for our current app config, so we can chunk this data instead. + paginator = Paginator(updated_transition_domains, batch_size) + for page_num in paginator.page_range: + page = paginator.page(page_num) + TransitionDomain.objects.bulk_update(page.object_list, updated_fields) + failed_count = len(failed_transition_domains) if failed_count == 0: if self.debug: diff --git a/src/registrar/migrations/0064_alter_domainapplication_address_line1_and_more.py b/src/registrar/migrations/0064_alter_domainapplication_address_line1_and_more.py new file mode 100644 index 000000000..7241c7164 --- /dev/null +++ b/src/registrar/migrations/0064_alter_domainapplication_address_line1_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.7 on 2024-01-23 22:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0063_veryimportantperson"), + ] + + operations = [ + migrations.AlterField( + model_name="domainapplication", + name="address_line1", + field=models.TextField(blank=True, help_text="Street address", null=True, verbose_name="Address line 1"), + ), + migrations.AlterField( + model_name="domainapplication", + name="address_line2", + field=models.TextField( + blank=True, help_text="Street address line 2 (optional)", null=True, verbose_name="Address line 2" + ), + ), + ] diff --git a/src/registrar/migrations/0065_create_groups_v06.py b/src/registrar/migrations/0065_create_groups_v06.py new file mode 100644 index 000000000..d2cb32cee --- /dev/null +++ b/src/registrar/migrations/0065_create_groups_v06.py @@ -0,0 +1,37 @@ +# This migration creates the create_full_access_group and create_cisa_analyst_group groups +# It is dependent on 0035 (which populates ContentType and Permissions) +# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS +# in the user_group model then: +# [NOT RECOMMENDED] +# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions +# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups +# step 3: fake run the latest migration in the migrations list +# [RECOMMENDED] +# Alternatively: +# step 1: duplicate the migration that loads data +# step 2: docker-compose exec app ./manage.py migrate + +from django.db import migrations +from registrar.models import UserGroup +from typing import Any + + +# For linting: RunPython expects a function reference, +# so let's give it one +def create_groups(apps, schema_editor) -> Any: + UserGroup.create_cisa_analyst_group(apps, schema_editor) + UserGroup.create_full_access_group(apps, schema_editor) + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0064_alter_domainapplication_address_line1_and_more"), + ] + + operations = [ + migrations.RunPython( + create_groups, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] diff --git a/src/registrar/migrations/0066_rename_veryimportantperson_verifiedbystaff_and_more.py b/src/registrar/migrations/0066_rename_veryimportantperson_verifiedbystaff_and_more.py new file mode 100644 index 000000000..d167334a0 --- /dev/null +++ b/src/registrar/migrations/0066_rename_veryimportantperson_verifiedbystaff_and_more.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.7 on 2024-01-29 22:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0065_create_groups_v06"), + ] + + operations = [ + migrations.RenameModel( + old_name="VeryImportantPerson", + new_name="VerifiedByStaff", + ), + migrations.AlterModelOptions( + name="verifiedbystaff", + options={"verbose_name_plural": "Verified by staff"}, + ), + ] diff --git a/src/registrar/migrations/0067_create_groups_v07.py b/src/registrar/migrations/0067_create_groups_v07.py new file mode 100644 index 000000000..85138d4af --- /dev/null +++ b/src/registrar/migrations/0067_create_groups_v07.py @@ -0,0 +1,37 @@ +# This migration creates the create_full_access_group and create_cisa_analyst_group groups +# It is dependent on 0035 (which populates ContentType and Permissions) +# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS +# in the user_group model then: +# [NOT RECOMMENDED] +# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions +# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups +# step 3: fake run the latest migration in the migrations list +# [RECOMMENDED] +# Alternatively: +# step 1: duplicate the migration that loads data +# step 2: docker-compose exec app ./manage.py migrate + +from django.db import migrations +from registrar.models import UserGroup +from typing import Any + + +# For linting: RunPython expects a function reference, +# so let's give it one +def create_groups(apps, schema_editor) -> Any: + UserGroup.create_cisa_analyst_group(apps, schema_editor) + UserGroup.create_full_access_group(apps, schema_editor) + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0066_rename_veryimportantperson_verifiedbystaff_and_more"), + ] + + operations = [ + migrations.RunPython( + create_groups, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] diff --git a/src/registrar/migrations/0068_domainapplication_notes_domaininformation_notes.py b/src/registrar/migrations/0068_domainapplication_notes_domaininformation_notes.py new file mode 100644 index 000000000..ea94be77e --- /dev/null +++ b/src/registrar/migrations/0068_domainapplication_notes_domaininformation_notes.py @@ -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), + ), + ] diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index 90cb2e286..d9ccd64cb 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -13,7 +13,7 @@ from .user import User from .user_group import UserGroup from .website import Website from .transition_domain import TransitionDomain -from .very_important_person import VeryImportantPerson +from .verified_by_staff import VerifiedByStaff __all__ = [ "Contact", @@ -30,7 +30,7 @@ __all__ = [ "UserGroup", "Website", "TransitionDomain", - "VeryImportantPerson", + "VerifiedByStaff", ] auditlog.register(Contact) @@ -47,4 +47,4 @@ auditlog.register(User, m2m_fields=["user_permissions", "groups"]) auditlog.register(UserGroup, m2m_fields=["permissions"]) auditlog.register(Website) auditlog.register(TransitionDomain) -auditlog.register(VeryImportantPerson) +auditlog.register(VerifiedByStaff) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 1a581a4ec..8a7f9c6e6 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -12,6 +12,7 @@ from django.utils import timezone from typing import Any from registrar.models.host import Host from registrar.models.host_ip import HostIP +from registrar.utility.enums import DefaultEmail from registrar.utility.errors import ( ActionNotAllowed, @@ -910,10 +911,15 @@ class Domain(TimeStampedModel, DomainHelper): raise NotImplementedError() def get_security_email(self): - logger.info("get_security_email-> getting the contact ") - secContact = self.security_contact - if secContact is not None: - return secContact.email + logger.info("get_security_email-> getting the contact") + + security = PublicContact.ContactTypeChoices.SECURITY + security_contact = self.generic_contact_getter(security) + + # If we get a valid value for security_contact, pull its email + # Otherwise, just return nothing + if security_contact is not None and isinstance(security_contact, PublicContact): + return security_contact.email else: return None @@ -1121,7 +1127,6 @@ class Domain(TimeStampedModel, DomainHelper): If you wanted to setup getter logic for Security, you would call: cache_contact_helper(PublicContact.ContactTypeChoices.SECURITY), or cache_contact_helper("security"). - """ # registrant_contact(s) are an edge case. They exist on # the "registrant" property as opposed to contacts. @@ -1400,7 +1405,9 @@ class Domain(TimeStampedModel, DomainHelper): is_security = contact.contact_type == contact.ContactTypeChoices.SECURITY DF = epp.DiscloseField 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 logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose) # Will only disclose DF.EMAIL if its not the default diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index f96ec1040..0399a039e 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -431,11 +431,13 @@ class DomainApplication(TimeStampedModel): null=True, blank=True, help_text="Street address", + verbose_name="Address line 1", ) address_line2 = models.TextField( null=True, blank=True, help_text="Street address line 2 (optional)", + verbose_name="Address line 2", ) city = models.TextField( null=True, @@ -556,6 +558,12 @@ class DomainApplication(TimeStampedModel): help_text="Date submitted", ) + notes = models.TextField( + null=True, + blank=True, + help_text="Notes about this request", + ) + def __str__(self): try: if self.requested_domain and self.requested_domain.name: @@ -570,6 +578,19 @@ class DomainApplication(TimeStampedModel): return not self.approved_domain.is_active() return True + def delete_and_clean_up_domain(self, called_from): + try: + domain_state = self.approved_domain.state + # Only reject if it exists on EPP + if domain_state != Domain.State.UNKNOWN: + self.approved_domain.deletedInEpp() + self.approved_domain.save() + self.approved_domain.delete() + self.approved_domain = None + except Exception as err: + logger.error(err) + logger.error(f"Can't query an approved domain while attempting {called_from}") + def _send_status_update_email(self, new_status, email_template, email_template_subject, send_email=True): """Send a status update email to the submitter. @@ -651,11 +672,19 @@ class DomainApplication(TimeStampedModel): ApplicationStatus.INELIGIBLE, ], target=ApplicationStatus.IN_REVIEW, + conditions=[domain_is_not_active], ) def in_review(self): """Investigate an application that has been submitted. - This action is logged.""" + This action is logged. + + As side effects this will delete the domain and domain_information + (will cascade) when they exist.""" + + if self.status == self.ApplicationStatus.APPROVED: + self.delete_and_clean_up_domain("in_review") + literal = DomainApplication.ApplicationStatus.IN_REVIEW # Check if the tuple exists, then grab its value in_review = literal if literal is not None else "In Review" @@ -670,11 +699,19 @@ class DomainApplication(TimeStampedModel): ApplicationStatus.INELIGIBLE, ], target=ApplicationStatus.ACTION_NEEDED, + conditions=[domain_is_not_active], ) def action_needed(self): """Send back an application that is under investigation or rejected. - This action is logged.""" + This action is logged. + + As side effects this will delete the domain and domain_information + (will cascade) when they exist.""" + + if self.status == self.ApplicationStatus.APPROVED: + self.delete_and_clean_up_domain("reject_with_prejudice") + literal = DomainApplication.ApplicationStatus.ACTION_NEEDED # Check if the tuple is setup correctly, then grab its value action_needed = literal if literal is not None else "Action Needed" @@ -707,7 +744,7 @@ class DomainApplication(TimeStampedModel): # copy the information from domainapplication into 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 UserDomainRole = apps.get_model("registrar.UserDomainRole") @@ -747,18 +784,9 @@ class DomainApplication(TimeStampedModel): As side effects this will delete the domain and domain_information (will cascade), and send an email notification.""" + if self.status == self.ApplicationStatus.APPROVED: - try: - domain_state = self.approved_domain.state - # Only reject if it exists on EPP - if domain_state != Domain.State.UNKNOWN: - self.approved_domain.deletedInEpp() - self.approved_domain.save() - self.approved_domain.delete() - self.approved_domain = None - except Exception as err: - logger.error(err) - logger.error("Can't query an approved domain while attempting a DA reject()") + self.delete_and_clean_up_domain("reject") self._send_status_update_email( "action needed", @@ -787,17 +815,7 @@ class DomainApplication(TimeStampedModel): and domain_information (will cascade) when they exist.""" if self.status == self.ApplicationStatus.APPROVED: - try: - domain_state = self.approved_domain.state - # Only reject if it exists on EPP - if domain_state != Domain.State.UNKNOWN: - self.approved_domain.deletedInEpp() - self.approved_domain.save() - self.approved_domain.delete() - self.approved_domain = None - except Exception as err: - logger.error(err) - logger.error("Can't query an approved domain while attempting a DA reject_with_prejudice()") + self.delete_and_clean_up_domain("reject_with_prejudice") self.creator.restrict_user() diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index bdff6061b..65d099e5a 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -1,4 +1,7 @@ from __future__ import annotations +from django.db import transaction + +from registrar.models.utility.domain_helper import DomainHelper from .domain_application import DomainApplication from .utility.time_stamped_model import TimeStampedModel @@ -202,6 +205,12 @@ class DomainInformation(TimeStampedModel): help_text="Acknowledged .gov acceptable use policy", ) + notes = models.TextField( + null=True, + blank=True, + help_text="Notes about the request", + ) + def __str__(self): try: if self.domain and self.domain.name: @@ -212,37 +221,63 @@ class DomainInformation(TimeStampedModel): return "" @classmethod - def create_from_da(cls, domain_application, domain=None): - """Takes in a DomainApplication dict and converts it into DomainInformation""" - da_dict = domain_application.to_dict() - # remove the id so one can be assinged on creation - da_id = da_dict.pop("id", None) + def create_from_da(cls, domain_application: DomainApplication, domain=None): + """Takes in a DomainApplication and converts it into DomainInformation""" + + # Throw an error if we get None - we can't create something from nothing + 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 # application, if so short circuit the create - domain_info = cls.objects.filter(domain_application__id=da_id).first() - if domain_info: - return 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() + existing_domain_info = cls.objects.filter(domain_application__id=domain_application.id).first() + if existing_domain_info: + return existing_domain_info - # Process the remaining "many to many" stuff - domain_info.other_contacts.add(*other_contacts) + # Get the fields that exist on both DomainApplication and DomainInformation + 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: 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 + @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: verbose_name_plural = "Domain information" diff --git a/src/registrar/models/public_contact.py b/src/registrar/models/public_contact.py index c49a66260..08891fa97 100644 --- a/src/registrar/models/public_contact.py +++ b/src/registrar/models/public_contact.py @@ -4,6 +4,8 @@ from string import ascii_uppercase, ascii_lowercase, digits from django.db import models +from registrar.utility.enums import DefaultEmail + from .utility.time_stamped_model import TimeStampedModel @@ -87,7 +89,7 @@ class PublicContact(TimeStampedModel): sp="VA", pc="20598-0645", cc="US", - email="dotgov@cisa.dhs.gov", + email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, voice="+1.8882820870", pw="thisisnotapassword", ) @@ -104,7 +106,7 @@ class PublicContact(TimeStampedModel): sp="VA", pc="22201", cc="US", - email="dotgov@cisa.dhs.gov", + email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, voice="+1.8882820870", pw="thisisnotapassword", ) @@ -121,7 +123,7 @@ class PublicContact(TimeStampedModel): sp="VA", pc="22201", cc="US", - email="dotgov@cisa.dhs.gov", + email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, voice="+1.8882820870", pw="thisisnotapassword", ) @@ -138,7 +140,7 @@ class PublicContact(TimeStampedModel): sp="VA", pc="22201", cc="US", - email="dotgov@cisa.dhs.gov", + email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, voice="+1.8882820870", pw="thisisnotapassword", ) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 269569bfe..bf904a044 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -7,7 +7,7 @@ from registrar.models.user_domain_role import UserDomainRole from .domain_invitation import DomainInvitation from .transition_domain import TransitionDomain -from .very_important_person import VeryImportantPerson +from .verified_by_staff import VerifiedByStaff from .domain import Domain from phonenumber_field.modelfields import PhoneNumberField # type: ignore @@ -91,7 +91,7 @@ class User(AbstractUser): return False # New users flagged by Staff to bypass ial2 - if VeryImportantPerson.objects.filter(email=email).exists(): + if VerifiedByStaff.objects.filter(email=email).exists(): return False # A new incoming user who is being invited to be a domain manager (that is, diff --git a/src/registrar/models/user_group.py b/src/registrar/models/user_group.py index a733239c7..a32406a05 100644 --- a/src/registrar/models/user_group.py +++ b/src/registrar/models/user_group.py @@ -66,6 +66,11 @@ class UserGroup(Group): "model": "userdomainrole", "permissions": ["view_userdomainrole", "delete_userdomainrole"], }, + { + "app_label": "registrar", + "model": "verifiedbystaff", + "permissions": ["add_verifiedbystaff", "change_verifiedbystaff", "delete_verifiedbystaff"], + }, ] # Avoid error: You can't execute queries until the end diff --git a/src/registrar/models/utility/domain_helper.py b/src/registrar/models/utility/domain_helper.py index a808ef803..230b23e16 100644 --- a/src/registrar/models/utility/domain_helper.py +++ b/src/registrar/models/utility/domain_helper.py @@ -1,5 +1,6 @@ import re - +from typing import Type +from django.db import models from django import forms from django.http import JsonResponse @@ -29,7 +30,6 @@ class DomainHelper: @classmethod def validate(cls, domain: str, blank_ok=False) -> str: """Attempt to determine if a domain name could be requested.""" - # Split into pieces for the linter domain = cls._validate_domain_string(domain, blank_ok) @@ -57,6 +57,9 @@ class DomainHelper: # If blank ok is true, just return the domain return domain + if domain.startswith("www."): + domain = domain[4:] + if domain.endswith(".gov"): domain = domain[:-4] @@ -158,3 +161,29 @@ class DomainHelper: """Get the top level domain. Example: `gsa.gov` -> `gov`.""" parts = domain.rsplit(".") 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 diff --git a/src/registrar/models/very_important_person.py b/src/registrar/models/verified_by_staff.py similarity index 85% rename from src/registrar/models/very_important_person.py rename to src/registrar/models/verified_by_staff.py index 9134cb893..4c9e76e9d 100644 --- a/src/registrar/models/very_important_person.py +++ b/src/registrar/models/verified_by_staff.py @@ -3,7 +3,7 @@ from django.db import models from .utility.time_stamped_model import TimeStampedModel -class VeryImportantPerson(TimeStampedModel): +class VerifiedByStaff(TimeStampedModel): """emails that get added to this table will bypass ial2 on login.""" @@ -28,5 +28,8 @@ class VeryImportantPerson(TimeStampedModel): help_text="Notes", ) + class Meta: + verbose_name_plural = "Verified by staff" + def __str__(self): return self.email diff --git a/src/registrar/templates/admin/base_site.html b/src/registrar/templates/admin/base_site.html index c0884c912..f9ff23455 100644 --- a/src/registrar/templates/admin/base_site.html +++ b/src/registrar/templates/admin/base_site.html @@ -24,34 +24,57 @@ {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} {% block extrastyle %}{{ block.super }} - + {% endblock %} -{% block branding %} -

.gov admin

-{% if user.is_anonymous %} - {% include "admin/color_theme_toggle.html" %} -{% endif %} +{% block header %} + {% if not IS_PRODUCTION %} + {% with add_body_class="margin-left-1" %} + {% include "includes/non-production-alert.html" %} + {% endwith %} + {% endif %} + + {# Djando update: this div will change to header #} + {% endblock %} -{% comment %} - This was copied from the 'userlinks' template, with a few minor changes. - You can find that here: - https://github.com/django/django/blob/d25f3892114466d689fd6936f79f3bd9a9acc30e/django/contrib/admin/templates/admin/base.html#L59 -{% endcomment %} -{% block userlinks %} - {% if site_url %} - {% translate 'View site' %} / - {% endif %} - {% if user.is_active and user.is_staff %} - {% url 'django-admindocs-docroot' as docsroot %} - {% if docsroot %} - {% translate 'Documentation' %} / - {% endif %} - {% endif %} - {% if user.has_usable_password %} - {% translate 'Change password' %} / - {% endif %} - {% translate 'Log out' %} - {% include "admin/color_theme_toggle.html" %} - {% endblock %} -{% block nav-global %}{% endblock %} \ No newline at end of file diff --git a/src/registrar/templates/application_dotgov_domain.html b/src/registrar/templates/application_dotgov_domain.html index 1838f33f4..155fdbeb4 100644 --- a/src/registrar/templates/application_dotgov_domain.html +++ b/src/registrar/templates/application_dotgov_domain.html @@ -11,7 +11,8 @@

-

Names that uniquely apply to your organization are likely to be approved over names that could also apply to other organizations. In most instances, this requires including your state’s two-letter abbreviation.

+

Names that uniquely apply to your organization 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 state’s two-letter abbreviation.{% endif %}

Requests for your organization’s initials or an abbreviated name might not be approved, but we encourage you to request the name you want.

@@ -48,16 +49,15 @@ {% endwith %} {% endwith %} {{ forms.1.management_form }} -
+

Alternative domains (optional)

@@ -66,23 +66,34 @@ you your first choice?

{% with attr_aria_describedby="alt_domain_instructions" %} - {# attr_validate / validate="domain" invokes code in get-gov.js #} - {# attr_auto_validate likewise triggers behavior in get-gov.js #} - {% with append_gov=True attr_validate="domain" attr_auto_validate=True %} - {% with add_class="blank-ok alternate-domain-input" %} - {% for form in forms.1 %} + {# Will probably want to remove blank-ok and do related cleanup when we implement delete #} + {% with attr_validate="domain" append_gov=True add_label_class="usa-sr-only" add_class="blank-ok alternate-domain-input" %} + {% for form in forms.1 %} +
{% input_with_errors form.alternative_domain %} - {% endfor %} - {% endwith %} +
+ {% endfor %} {% endwith %} {% endwith %} - -
+
+ +
+ + +

If you’re not sure this is the domain you want, that’s ok. You can change the domain later.

+ +
{% endblock %} diff --git a/src/registrar/templates/application_form.html b/src/registrar/templates/application_form.html index 524045fbe..965967072 100644 --- a/src/registrar/templates/application_form.html +++ b/src/registrar/templates/application_form.html @@ -1,7 +1,7 @@ {% extends 'base.html' %} {% 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 %}
diff --git a/src/registrar/templates/application_review.html b/src/registrar/templates/application_review.html index 974830e91..4af6b758f 100644 --- a/src/registrar/templates/application_review.html +++ b/src/registrar/templates/application_review.html @@ -20,108 +20,158 @@ {% block form_fields %} {% for step in steps.all|slice:":-1" %} -
-
-
-
-
{{ form_titles|get_item:step }}
-
- {% if step == Step.ORGANIZATION_TYPE %} - {% if application.organization_type is not None %} - {% with long_org_type=application.organization_type|get_organization_long_name %} - {{ long_org_type }} - {% endwith %} - {% else %} - Incomplete - {% endif %} +
+ + {% if step == Step.ORGANIZATION_TYPE %} + {% namespaced_url 'application' step as application_url %} + {% if application.organization_type is not None %} + {% with title=form_titles|get_item:step value=application.get_organization_type_display|default:"Incomplete" %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %} + {% endwith %} + {% else %} + {% with title=form_titles|get_item:step value="Incomplete" %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %} + {% endwith %} {% endif %} - {% if step == Step.TRIBAL_GOVERNMENT %} - {{ application.tribe_name|default:"Incomplete" }} - {% if application.federally_recognized_tribe %}

Federally-recognized tribe

{% endif %} - {% if application.state_recognized_tribe %}

State-recognized tribe

{% endif %} + {% endif %} + + {% if step == Step.TRIBAL_GOVERNMENT %} + {% namespaced_url 'application' step as application_url %} + {% with title=form_titles|get_item:step value=application.tribe_name|default:"Incomplete" %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %} + {% endwith %} + {% if application.federally_recognized_tribe %}

Federally-recognized tribe

{% endif %} + {% if application.state_recognized_tribe %}

State-recognized tribe

{% endif %} + {% endif %} + + + {% if step == Step.ORGANIZATION_FEDERAL %} + {% namespaced_url 'application' step as application_url %} + {% with title=form_titles|get_item:step value=application.get_federal_type_display|default:"Incomplete" %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %} + {% endwith %} + {% endif %} + + {% if step == Step.ORGANIZATION_ELECTION %} + {% namespaced_url 'application' step as application_url %} + {% with title=form_titles|get_item:step value=application.is_election_board|yesno:"Yes,No,Incomplete" %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %} + {% endwith %} + {% endif %} + + {% if step == Step.ORGANIZATION_CONTACT %} + {% namespaced_url 'application' step as application_url %} + {% if application.organization_name %} + {% with title=form_titles|get_item:step value=application %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url address='true' %} + {% endwith %} + {% else %} + {% with title=form_titles|get_item:step value='Incomplete' %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %} + {% endwith %} {% endif %} - {% if step == Step.ORGANIZATION_FEDERAL %} - {{ application.get_federal_type_display|default:"Incomplete" }} + {% endif %} + + {% if step == Step.ABOUT_YOUR_ORGANIZATION %} + {% namespaced_url 'application' step as application_url %} + {% with title=form_titles|get_item:step value=application.about_your_organization|default:"Incomplete" %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %} + {% endwith %} + {% endif %} + + {% if step == Step.AUTHORIZING_OFFICIAL %} + {% namespaced_url 'application' step as application_url %} + {% if application.authorizing_official is not None %} + {% with title=form_titles|get_item:step value=application.authorizing_official %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url contact='true' %} + {% endwith %} + {% else %} + {% with title=form_titles|get_item:step value="Incomplete" %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %} + {% endwith %} {% endif %} - {% if step == Step.ORGANIZATION_ELECTION %} - {{ application.is_election_board|yesno:"Yes,No,Incomplete" }} + {% endif %} + + {% if step == Step.CURRENT_SITES %} + {% namespaced_url 'application' step as application_url %} + {% if application.current_websites.all %} + {% with title=form_titles|get_item:step value=application.current_websites.all %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url list='true' %} + {% endwith %} + {% else %} + {% with title=form_titles|get_item:step value='None' %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %} + {% endwith %} {% endif %} - {% if step == Step.ORGANIZATION_CONTACT %} - {% if application.organization_name %} - {% include "includes/organization_address.html" with organization=application %} - {% else %} - Incomplete - {% endif %} - {% endif %} - {% if step == Step.ABOUT_YOUR_ORGANIZATION %} -

{{ application.about_your_organization|default:"Incomplete" }}

- {% endif %} - {% if step == Step.AUTHORIZING_OFFICIAL %} - {% if application.authorizing_official %} -
- {% include "includes/contact.html" with contact=application.authorizing_official %} -
- {% else %} - Incomplete - {% endif %} - {% endif %} - {% if step == Step.CURRENT_SITES %} -
    - {% for site in application.current_websites.all %} -
  • {{ site.website }}
  • - {% empty %} -
  • None
  • - {% endfor %} -
- {% endif %} - {% if step == Step.DOTGOV_DOMAIN %} -
    -
  • {{ application.requested_domain.name|default:"Incomplete" }}
  • -
-
    + {% endif %} + + {% if step == Step.DOTGOV_DOMAIN %} + {% namespaced_url 'application' step as application_url %} + {% with title=form_titles|get_item:step value=application.requested_domain.name|default:"Incomplete" %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %} + {% endwith %} + + {% if application.alternative_domains.all %} +

    Alternative domains

    +
      {% for site in application.alternative_domains.all %}
    • {{ site.website }}
    • {% endfor %}
    {% endif %} - {% if step == Step.PURPOSE %} - {{ application.purpose|default:"Incomplete" }} + {% endif %} + + {% if step == Step.PURPOSE %} + {% namespaced_url 'application' step as application_url %} + {% with title=form_titles|get_item:step value=application.purpose|default:"Incomplete" %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %} + {% endwith %} + {% endif %} + + {% if step == Step.YOUR_CONTACT %} + {% namespaced_url 'application' step as application_url %} + {% if application.submitter is not None %} + {% with title=form_titles|get_item:step value=application.submitter %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url contact='true' %} + {% endwith %} + {% else %} + {% with title=form_titles|get_item:step value="Incomplete" %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %} + {% endwith %} {% endif %} - {% if step == Step.YOUR_CONTACT %} - {% if application.submitter %} -
    - {% include "includes/contact.html" with contact=application.submitter %} -
    - {% else %} - Incomplete - {% endif %} + {% endif %} + + {% if step == Step.OTHER_CONTACTS %} + {% namespaced_url 'application' step as application_url %} + {% if application.other_contacts.all %} + {% with title=form_titles|get_item:step value=application.other_contacts.all %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url contact='true' list='true' %} + {% endwith %} + {% else %} + {% with title=form_titles|get_item:step value=application.no_other_contacts_rationale|default:"Incomplete" %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %} + {% endwith %} {% endif %} - {% if step == Step.OTHER_CONTACTS %} - {% for other in application.other_contacts.all %} -
    -

    Contact {{ forloop.counter }}

    - {% include "includes/contact.html" with contact=other %} -
    - {% empty %} -
    -

    No other employees from your organization?

    - {{ application.no_other_contacts_rationale|default:"Incomplete" }} -
    - {% endfor %} - {% endif %} - {% if step == Step.ANYTHING_ELSE %} - {{ application.anything_else|default:"No" }} - {% endif %} - {% if step == Step.REQUIREMENTS %} - {{ application.is_policy_acknowledged|yesno:"I agree.,I do not agree.,I do not agree." }} - {% endif %} -
-
- Edit {{ form_titles|get_item:step }} -
+ {% endif %} + + + {% if step == Step.ANYTHING_ELSE %} + {% namespaced_url 'application' step as application_url %} + {% with title=form_titles|get_item:step value=application.anything_else|default:"No" %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %} + {% endwith %} + {% endif %} + + + {% if step == Step.REQUIREMENTS %} + {% namespaced_url 'application' step as application_url %} + {% with title=form_titles|get_item:step value=application.is_policy_acknowledged|yesno:"I agree.,I do not agree.,I do not agree." %} + {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %} + {% endwith %} + {% endif %} + + +
{% endfor %} {% endblock %} diff --git a/src/registrar/templates/application_sidebar.html b/src/registrar/templates/application_sidebar.html index 318bea366..e0e51dd45 100644 --- a/src/registrar/templates/application_sidebar.html +++ b/src/registrar/templates/application_sidebar.html @@ -8,10 +8,12 @@
  • {% if not this_step == steps.current %} - + {% if this_step != "review" %} + + {% endif %} {% endif %}

    Summary of your domain request

    {% with heading_level='h3' %} - {% with long_org_type=domainapplication.organization_type|get_organization_long_name %} - {% include "includes/summary_item.html" with title='Type of organization' value=long_org_type heading_level=heading_level %} + {% with org_type=domainapplication.get_organization_type_display %} + {% include "includes/summary_item.html" with title='Type of organization' value=org_type heading_level=heading_level %} {% endwith %} {% if domainapplication.tribe_name %} @@ -74,7 +74,9 @@ {% endif %} {% if domainapplication.is_election_board %} - {% include "includes/summary_item.html" with title='Election office' value=domainapplication.is_election_board heading_level=heading_level %} + {% with value=domainapplication.is_election_board|yesno:"Yes,No,Incomplete" %} + {% include "includes/summary_item.html" with title='Election office' value=value heading_level=heading_level %} + {% endwith %} {% endif %} {% if domainapplication.organization_name %} @@ -109,8 +111,12 @@ {% include "includes/summary_item.html" with title='Your contact information' value=domainapplication.submitter contact='true' heading_level=heading_level %} {% endif %} - {% include "includes/summary_item.html" with title='Other employees from your organization' value=domainapplication.other_contacts.all contact='true' list='true' heading_level=heading_level %} - + {% if domainapplication.other_contacts.all %} + {% include "includes/summary_item.html" with title='Other employees from your organization' value=domainapplication.other_contacts.all contact='true' list='true' heading_level=heading_level %} + {% else %} + {% include "includes/summary_item.html" with title='Other employees from your organization' value=domainapplication.no_other_contacts_rationale heading_level=heading_level %} + {% endif %} + {% include "includes/summary_item.html" with title='Anything else?' value=domainapplication.anything_else|default:"No" heading_level=heading_level %} {% endwith %} diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index 2786cca22..c0702e78f 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -70,6 +70,10 @@
    Skip to main content + {% if not IS_PRODUCTION %} + {% include "includes/non-production-alert.html" %} + {% endif %} +
    diff --git a/src/registrar/templates/dashboard_base.html b/src/registrar/templates/dashboard_base.html index 27b5ea717..6dd2ce8fd 100644 --- a/src/registrar/templates/dashboard_base.html +++ b/src/registrar/templates/dashboard_base.html @@ -3,22 +3,22 @@ {% block wrapper %}
    - {% block messages %} - {% if messages %} -
      - {% for message in messages %} - - {{ message }} - - {% endfor %} -
    - {% endif %} - {% endblock %} - {% block section_nav %}{% endblock %} {% block hero %}{% endblock %} - {% block content %}{% endblock %} + {% block content %} + {% block messages %} + {% if messages %} +
      + {% for message in messages %} +
    • + {{ message }} +
    • + {% endfor %} +
    + {% endif %} + {% endblock %} + {% endblock %}
    {% block complementary %}{% endblock %}
    diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index 09fc189e4..4dcef69ae 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -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 %} {% 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 %} {% else %} {% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url editable=domain.is_editable %} diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 0eecd35b3..e295d2f7e 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -16,10 +16,8 @@
  • There is no limit to the number of domain managers you can add.
  • After adding a domain manager, an email invitation will be sent to that user with instructions on how to set up an account.
  • -
  • To remove a domain manager, contact us for - assistance.
  • All domain managers must keep their contact information updated and be responsive if contacted by the .gov team.
  • +
  • Domains must have at least one domain manager. You can’t remove yourself as a domain manager if you’re the only one assigned to this domain. Add another domain manager before you remove yourself from this domain.
  • {% if domain.permissions %} @@ -30,7 +28,8 @@ Email - Role + Role + Action @@ -40,6 +39,61 @@ {{ permission.user.email }} {{ permission.role|title }} + + {% if can_delete_users %} + + Remove + + {# Display a custom message if the user is trying to delete themselves #} + {% if permission.user.email == current_user_email %} +
    +
    + {% 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 "|add:domain_name|add:"."|safe modal_button=modal_button_self|safe %} + {% endwith %} +
    +
    + {% else %} +
    +
    + {% 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=""|add:email|add:" will no longer be able to manage the domain "|add:domain_name|add:"."|safe modal_button=modal_button|safe %} + {% endwith %} +
    +
    + {% endif %} + {% else %} + + {% endif %} + {% endfor %} @@ -66,8 +120,8 @@ Email Date created - Status - Action + Status + Action @@ -78,8 +132,9 @@ {{ invitation.created_at|date }} {{ invitation.status|title }} -
    - {% csrf_token %} + + + {% csrf_token %}
    diff --git a/src/registrar/templates/emails/domain_invitation.txt b/src/registrar/templates/emails/domain_invitation.txt index b9e4ba853..8896bd85f 100644 --- a/src/registrar/templates/emails/domain_invitation.txt +++ b/src/registrar/templates/emails/domain_invitation.txt @@ -1,7 +1,7 @@ {% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #} Hi. -{{ requester_email }} has added you as a manager on {{ domain.name }}. +{{ requestor_email }} has added you as a manager on {{ domain.name }}. You can manage this domain on the .gov registrar . diff --git a/src/registrar/templates/emails/includes/application_summary.txt b/src/registrar/templates/emails/includes/application_summary.txt index ee2564613..707689812 100644 --- a/src/registrar/templates/emails/includes/application_summary.txt +++ b/src/registrar/templates/emails/includes/application_summary.txt @@ -2,9 +2,22 @@ SUMMARY OF YOUR DOMAIN REQUEST Type of organization: {{ application.get_organization_type_display }} - +{% if application.show_organization_federal %} +Federal government branch: +{{ application.get_federal_type_display }} +{% elif application.show_tribal_government %} +Tribal government: +{{ application.tribe_name|default:"Incomplete" }}{% if application.federally_recognized_tribe %} +Federally-recognized tribe +{% endif %}{% if application.state_recognized_tribe %} +State-recognized tribe +{% endif %}{% endif %}{% if application.show_organization_election %} +Election office: +{{ application.is_election_board|yesno:"Yes,No,Incomplete" }} +{% endif %} Organization name and mailing address: -{% spaceless %}{{ application.organization_name }} +{% spaceless %}{{ application.federal_agency }} +{{ application.organization_name }} {{ application.address_line1 }}{% if application.address_line2 %} {{ application.address_line2 }}{% endif %} {{ application.city }}, {{ application.state_territory }} @@ -22,18 +35,21 @@ Current websites: {% for site in application.current_websites.all %} {% endfor %}{% endif %} .gov domain: {{ application.requested_domain.name }} +{% if application.alternative_domains.all %} +Alternative domains: {% for site in application.alternative_domains.all %}{% spaceless %}{{ site.website }}{% endspaceless %} -{% endfor %} +{% endfor %}{% endif %} Purpose of your domain: {{ application.purpose }} Your contact information: {% spaceless %}{% include "emails/includes/contact.txt" with contact=application.submitter %}{% endspaceless %} -{% if application.other_contacts.all %} -Other employees from your organization: -{% for other in application.other_contacts.all %} + +Other employees from your organization:{% for other in application.other_contacts.all %} {% spaceless %}{% include "emails/includes/contact.txt" with contact=other %}{% endspaceless %} -{% endfor %}{% endif %}{% if application.anything_else %} +{% empty %} +{{ application.no_other_contacts_rationale }} +{% endfor %}{% if application.anything_else %} Anything else? {{ application.anything_else }} {% endif %} \ No newline at end of file diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index c7a005f97..2400e38f3 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -10,6 +10,9 @@ {# the entire logged in page goes here #}
    + {% block messages %} + {% include "includes/form_messages.html" %} + {% endblock %}

    Manage your domains

    diff --git a/src/registrar/templates/includes/form_messages.html b/src/registrar/templates/includes/form_messages.html index c7b704f67..59ecb4eaa 100644 --- a/src/registrar/templates/includes/form_messages.html +++ b/src/registrar/templates/includes/form_messages.html @@ -2,7 +2,7 @@ {% for message in messages %}

    - {{ message }} + {{ message }}
    diff --git a/src/registrar/templates/includes/non-production-alert.html b/src/registrar/templates/includes/non-production-alert.html new file mode 100644 index 000000000..8e40892bc --- /dev/null +++ b/src/registrar/templates/includes/non-production-alert.html @@ -0,0 +1,5 @@ +
    +
    + Attention: You are on a test site. +
    +
    diff --git a/src/registrar/templates/includes/organization_address.html b/src/registrar/templates/includes/organization_address.html index 52f0d437a..c0baf3c9f 100644 --- a/src/registrar/templates/includes/organization_address.html +++ b/src/registrar/templates/includes/organization_address.html @@ -1,4 +1,7 @@
    + {% if organization.federal_agency %} + {{ organization.federal_agency }}
    + {% endif %} {% if organization.organization_name %} {{ organization.organization_name }} {% endif %} diff --git a/src/registrar/templates/includes/summary_item.html b/src/registrar/templates/includes/summary_item.html index 53364d1b2..7c0d801ad 100644 --- a/src/registrar/templates/includes/summary_item.html +++ b/src/registrar/templates/includes/summary_item.html @@ -28,17 +28,22 @@ {% if value|length == 1 %} {% include "includes/contact.html" with contact=value|first %} {% else %} -
      - {% for item in value %} -
    • -

      - Contact {{forloop.counter}} -

      - {% include "includes/contact.html" with contact=item %}
    • - {% empty %} -
    • None
    • - {% endfor %} -
    + {% if value %} +
    + {% for item in value %} +
    + Contact {{forloop.counter}} +
    +
    + {% include "includes/contact.html" with contact=item %} +
    + {% endfor %} +
    + {% else %} +

    + None +

    + {% endif %} {% endif %} {% else %} {% include "includes/contact.html" with contact=value %} @@ -57,10 +62,10 @@ {% endspaceless %}) {% endif %} {% else %} -

    {{ value | first }}

    +

    {{ value | first }}

    {% endif %} {% else %} -
      +
        {% for item in value %} {% if users %}
      • {{ item.user.email }}
      • diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py index 80ec5ef3d..2865bf5c5 100644 --- a/src/registrar/tests/common.py +++ b/src/registrar/tests/common.py @@ -12,6 +12,7 @@ from typing import List, Dict from django.contrib.sessions.middleware import SessionMiddleware from django.conf import settings from django.contrib.auth import get_user_model, login +from django.utils.timezone import make_aware from registrar.models import ( Contact, @@ -526,6 +527,7 @@ def completed_application( has_anything_else=True, status=DomainApplication.ApplicationStatus.STARTED, user=False, + submitter=False, name="city.gov", ): """A completed domain application.""" @@ -541,13 +543,14 @@ def completed_application( domain, _ = DraftDomain.objects.get_or_create(name=name) alt, _ = Website.objects.get_or_create(website="city1.gov") current, _ = Website.objects.get_or_create(website="city.com") - you, _ = Contact.objects.get_or_create( - first_name="Testy2", - last_name="Tester2", - title="Admin Tester", - email="mayor@igorville.gov", - phone="(555) 555 5556", - ) + if not submitter: + submitter, _ = Contact.objects.get_or_create( + first_name="Testy2", + last_name="Tester2", + title="Admin Tester", + email="mayor@igorville.gov", + phone="(555) 555 5556", + ) other, _ = Contact.objects.get_or_create( first_name="Testy", last_name="Tester", @@ -567,7 +570,7 @@ def completed_application( zipcode="10002", authorizing_official=ao, requested_domain=domain, - submitter=you, + submitter=submitter, creator=user, status=status, ) @@ -641,7 +644,7 @@ class MockEppLib(TestCase): self, id, 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", ): fake = info.InfoContactResultData( @@ -679,7 +682,7 @@ class MockEppLib(TestCase): mockDataInfoDomain = fakedEppObject( "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)], hosts=["fake.host.com"], statuses=[ @@ -690,7 +693,7 @@ class MockEppLib(TestCase): ) mockDataExtensionDomain = fakedEppObject( "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)], hosts=["fake.host.com"], statuses=[ @@ -704,7 +707,7 @@ class MockEppLib(TestCase): ) InfoDomainWithContacts = fakedEppObject( "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="securityContact", @@ -729,7 +732,7 @@ class MockEppLib(TestCase): InfoDomainWithDefaultSecurityContact = fakedEppObject( "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="defaultSec", @@ -748,7 +751,7 @@ class MockEppLib(TestCase): ) InfoDomainWithVerisignSecurityContact = fakedEppObject( "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="defaultVeri", @@ -764,7 +767,7 @@ class MockEppLib(TestCase): InfoDomainWithDefaultTechnicalContact = fakedEppObject( "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="defaultTech", @@ -789,14 +792,14 @@ class MockEppLib(TestCase): infoDomainNoContact = fakedEppObject( "security", - cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35), + cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)), contacts=[], hosts=["fake.host.com"], ) infoDomainThreeHosts = fakedEppObject( "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=[], hosts=[ "ns1.my-nameserver-1.com", @@ -807,25 +810,25 @@ class MockEppLib(TestCase): infoDomainNoHost = fakedEppObject( "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=[], hosts=[], ) infoDomainTwoHosts = fakedEppObject( "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=[], hosts=["ns1.my-nameserver-1.com", "ns1.my-nameserver-2.com"], ) mockDataInfoHosts = fakedEppObject( "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")], ) - 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 = { "keyTag": 1234, "alg": 3, @@ -857,7 +860,7 @@ class MockEppLib(TestCase): infoDomainHasIP = fakedEppObject( "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=[ common.DomainContact( contact="securityContact", @@ -882,7 +885,7 @@ class MockEppLib(TestCase): justNameserver = fakedEppObject( "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=[ common.DomainContact( contact="securityContact", @@ -905,7 +908,7 @@ class MockEppLib(TestCase): infoDomainCheckHostIPCombo = fakedEppObject( "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=[], hosts=[ "ns1.nameserversubdomain.gov", diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 83f777189..a5ea89072 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -14,11 +14,11 @@ from registrar.admin import ( ContactAdmin, DomainInformationAdmin, UserDomainRoleAdmin, - VeryImportantPersonAdmin, + VerifiedByStaffAdmin, ) from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website from registrar.models.user_domain_role import UserDomainRole -from registrar.models.very_important_person import VeryImportantPerson +from registrar.models.verified_by_staff import VerifiedByStaff from .common import ( MockSESClient, AuditedAdminMockData, @@ -59,22 +59,22 @@ class TestDomainAdmin(MockEppLib): """ Make sure the short name is displaying in admin on the list page """ - self.client.force_login(self.superuser) - application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): + with less_console_noise(): + self.client.force_login(self.superuser) + application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW) + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): 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 - # for our actual application - self.assertContains(response, "Federal", count=4) - # This may be a bit more robust - self.assertContains(response, 'Federal', count=1) - # Now let's make sure the long description does not exist - self.assertNotContains(response, "Federal: an agency of the U.S. government") + # There are 3 template references to Federal (3) plus one reference in the table + # for our actual application + self.assertContains(response, "Federal", count=4) + # This may be a bit more robust + self.assertContains(response, 'Federal', count=1) + # Now let's make sure the long description does not exist + self.assertNotContains(response, "Federal: an agency of the U.S. government") @skip("Why did this test stop working, and is is a good test") 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 And `state` is et to `DELETED` """ - domain = create_ready_domain() - # Put in client hold - domain.place_client_hold() - p = "userpass" - self.client.login(username="staffuser", password=p) - - # Ensure everything is displaying correctly - response = self.client.get( - "/admin/registrar/domain/{}/change/".format(domain.pk), - 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, + with less_console_noise(): + domain = create_ready_domain() + # Put in client hold + domain.place_client_hold() + p = "userpass" + self.client.login(username="staffuser", password=p) + # Ensure everything is displaying correctly + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.pk), + follow=True, ) - - self.assertEqual(domain.state, Domain.State.DELETED) + 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(domain.state, Domain.State.DELETED) 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 And `state` is not set to `DELETED` """ - domain = create_ready_domain() - p = "userpass" - self.client.login(username="staffuser", password=p) - - # Ensure everything is displaying correctly - response = self.client.get( - "/admin/registrar/domain/{}/change/".format(domain.pk), - 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, + with less_console_noise(): + domain = create_ready_domain() + p = "userpass" + self.client.login(username="staffuser", password=p) + # Ensure everything is displaying correctly + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.pk), + 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(domain.state, Domain.State.READY) @@ -205,62 +200,57 @@ class TestDomainAdmin(MockEppLib): Then `commands.DeleteDomain` is sent to the registry And Domain returns normally without an error dialog """ - domain = create_ready_domain() - # Put in client hold - domain.place_client_hold() - p = "userpass" - self.client.login(username="staffuser", password=p) - - # Ensure everything is displaying correctly - response = self.client.get( - "/admin/registrar/domain/{}/change/".format(domain.pk), - 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, + with less_console_noise(): + domain = create_ready_domain() + # Put in client hold + domain.place_client_hold() + p = "userpass" + self.client.login(username="staffuser", password=p) + # Ensure everything is displaying correctly + response = self.client.get( + "/admin/registrar/domain/{}/change/".format(domain.pk), + follow=True, ) - - 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(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(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") def test_place_and_remove_hold_epp(self): @@ -669,6 +659,7 @@ class TestDomainApplicationAdmin(MockEppLib): "anything_else", "is_policy_acknowledged", "submission_date", + "notes", "current_websites", "other_contacts", "alternative_domains", @@ -686,6 +677,7 @@ class TestDomainApplicationAdmin(MockEppLib): "creator", "about_your_organization", "requested_domain", + "approved_domain", "alternative_domains", "purpose", "submitter", @@ -759,33 +751,13 @@ class TestDomainApplicationAdmin(MockEppLib): application.approved_domain = domain application.save() - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - request.user = self.superuser + def trigger_saving_approved_to_another_state(self, domain_is_active, another_state): + """Helper method that triggers domain request state changes from approved to another state, + with an associated domain that can be either active (READY) or not. - # Define a custom implementation for is_active - def custom_is_active(self): - return True # Override to return True + Used to test errors when saving a change with an active domain, also used to test side effects + when saving a change goes through.""" - # Use ExitStack to combine patch contexts - with ExitStack() as stack: - # Patch Domain.is_active and django.contrib.messages.error simultaneously - stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) - stack.enter_context(patch.object(messages, "error")) - - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Simulate saving the model - application.status = DomainApplication.ApplicationStatus.REJECTED - self.admin.save_model(request, application, None, True) - - # Assert that the error message was called with the correct argument - messages.error.assert_called_once_with( - request, - "This action is not permitted. The domain " + "is already active.", - ) - - def test_side_effects_when_saving_approved_to_rejected(self): # Create an instance of the model application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) domain = Domain.objects.create(name=application.requested_domain.name) @@ -799,101 +771,60 @@ class TestDomainApplicationAdmin(MockEppLib): # Define a custom implementation for is_active def custom_is_active(self): - return False # Override to return False + return domain_is_active # Override to return True # Use ExitStack to combine patch contexts with ExitStack() as stack: # Patch Domain.is_active and django.contrib.messages.error simultaneously stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) stack.enter_context(patch.object(messages, "error")) - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): - # Simulate saving the model - application.status = DomainApplication.ApplicationStatus.REJECTED - self.admin.save_model(request, application, None, True) - # Assert that the error message was never called - messages.error.assert_not_called() + application.status = another_state + self.admin.save_model(request, application, None, True) - self.assertEqual(application.approved_domain, None) + # Assert that the error message was called with the correct argument + if domain_is_active: + messages.error.assert_called_once_with( + request, + "This action is not permitted. The domain " + "is already active.", + ) + else: + # Assert that the error message was never called + messages.error.assert_not_called() - # Assert that Domain got Deleted - with self.assertRaises(Domain.DoesNotExist): - domain.refresh_from_db() + self.assertEqual(application.approved_domain, None) - # Assert that DomainInformation got Deleted - with self.assertRaises(DomainInformation.DoesNotExist): - domain_information.refresh_from_db() + # Assert that Domain got Deleted + with self.assertRaises(Domain.DoesNotExist): + domain.refresh_from_db() + + # Assert that DomainInformation got Deleted + with self.assertRaises(DomainInformation.DoesNotExist): + domain_information.refresh_from_db() + + def test_error_when_saving_approved_to_in_review_and_domain_is_active(self): + self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.IN_REVIEW) + + def test_error_when_saving_approved_to_action_needed_and_domain_is_active(self): + self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.ACTION_NEEDED) + + def test_error_when_saving_approved_to_rejected_and_domain_is_active(self): + self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.REJECTED) def test_error_when_saving_approved_to_ineligible_and_domain_is_active(self): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - domain = Domain.objects.create(name=application.requested_domain.name) - application.approved_domain = domain - application.save() + self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.INELIGIBLE) - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - request.user = self.superuser + def test_side_effects_when_saving_approved_to_in_review(self): + self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.IN_REVIEW) - # Define a custom implementation for is_active - def custom_is_active(self): - return True # Override to return True + def test_side_effects_when_saving_approved_to_action_needed(self): + self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.ACTION_NEEDED) - # Use ExitStack to combine patch contexts - with ExitStack() as stack: - # Patch Domain.is_active and django.contrib.messages.error simultaneously - stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) - stack.enter_context(patch.object(messages, "error")) - - # Simulate saving the model - application.status = DomainApplication.ApplicationStatus.INELIGIBLE - self.admin.save_model(request, application, None, True) - - # Assert that the error message was called with the correct argument - messages.error.assert_called_once_with( - request, - "This action is not permitted. The domain " + "is already active.", - ) + def test_side_effects_when_saving_approved_to_rejected(self): + self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.REJECTED) def test_side_effects_when_saving_approved_to_ineligible(self): - # Create an instance of the model - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED) - domain = Domain.objects.create(name=application.requested_domain.name) - domain_information = DomainInformation.objects.create(creator=self.superuser, domain=domain) - application.approved_domain = domain - application.save() - - # Create a request object with a superuser - request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk)) - request.user = self.superuser - - # Define a custom implementation for is_active - def custom_is_active(self): - return False # Override to return False - - # Use ExitStack to combine patch contexts - with ExitStack() as stack: - # Patch Domain.is_active and django.contrib.messages.error simultaneously - stack.enter_context(patch.object(Domain, "is_active", custom_is_active)) - stack.enter_context(patch.object(messages, "error")) - - # Simulate saving the model - application.status = DomainApplication.ApplicationStatus.INELIGIBLE - self.admin.save_model(request, application, None, True) - - # Assert that the error message was never called - messages.error.assert_not_called() - - self.assertEqual(application.approved_domain, None) - - # Assert that Domain got Deleted - with self.assertRaises(Domain.DoesNotExist): - domain.refresh_from_db() - - # Assert that DomainInformation got Deleted - with self.assertRaises(DomainInformation.DoesNotExist): - domain_information.refresh_from_db() + self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.INELIGIBLE) def test_has_correct_filters(self): """ @@ -1110,7 +1041,7 @@ class DomainInvitationAdminTest(TestCase): self.assertContains(response, retrieved_html, count=1) -class DomainInformationAdminTest(TestCase): +class TestDomainInformationAdmin(TestCase): def setUp(self): """Setup environment for a mock admin user""" self.site = AdminSite() @@ -1118,6 +1049,7 @@ class DomainInformationAdminTest(TestCase): self.admin = DomainInformationAdmin(model=DomainInformation, admin_site=self.site) self.client = Client(HTTP_HOST="localhost:8080") self.superuser = create_superuser() + self.staffuser = create_user() self.mock_data_generator = AuditedAdminMockData() self.test_helper = GenericTestHelper( @@ -1161,6 +1093,27 @@ class DomainInformationAdminTest(TestCase): Contact.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): """Tests if DomainInformation sorts by domain correctly""" p = "adminpass" @@ -1325,64 +1278,62 @@ class ListHeaderAdminTest(TestCase): self.superuser = create_superuser() def test_changelist_view(self): - # Have to get creative to get past linter - p = "adminpass" - self.client.login(username="superuser", password=p) - - # Mock a user - user = mock_user() - - # Make the request using the Client class - # which handles CSRF - # Follow=True handles the redirect - response = self.client.get( - "/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"}, + with less_console_noise(): + # Have to get creative to get past linter + p = "adminpass" + self.client.login(username="superuser", password=p) + # Mock a user + user = mock_user() + # Make the request using the Client class + # which handles CSRF + # Follow=True handles the redirect + response = self.client.get( + "/admin/registrar/domainapplication/", { - "parameter_name": "investigator", - "parameter_value": user.first_name + " " + user.last_name, + "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", + "parameter_value": user.first_name + " " + user.last_name, + }, + ], + ) def test_get_filters(self): - # Create a mock request object - request = self.factory.get("/admin/yourmodel/") - # Set the GET parameters for testing - request.GET = { - "status": "started", - "investigator": "Jeff Lebowski", - "q": "search_value", - } - # Call the get_filters method - filters = self.admin.get_filters(request) - - # Assert the filters extracted from the request GET - self.assertEqual( - filters, - [ - {"parameter_name": "status", "parameter_value": "started"}, - {"parameter_name": "investigator", "parameter_value": "Jeff Lebowski"}, - ], - ) + with less_console_noise(): + # Create a mock request object + request = self.factory.get("/admin/yourmodel/") + # Set the GET parameters for testing + request.GET = { + "status": "started", + "investigator": "Jeff Lebowski", + "q": "search_value", + } + # Call the get_filters method + filters = self.admin.get_filters(request) + # Assert the filters extracted from the request GET + self.assertEqual( + filters, + [ + {"parameter_name": "status", "parameter_value": "started"}, + {"parameter_name": "investigator", "parameter_value": "Jeff Lebowski"}, + ], + ) def tearDown(self): # delete any applications too @@ -1781,11 +1732,86 @@ class ContactAdminTest(TestCase): self.assertEqual(readonly_fields, expected_fields) + def test_change_view_for_joined_contact_five_or_less(self): + """Create a contact, join it to 4 domain requests. The 5th join will be a user. + Assert that the warning on the contact form lists 5 joins.""" + + self.client.force_login(self.superuser) + + # Create an instance of the model + contact, _ = Contact.objects.get_or_create(user=self.staffuser) + + # join it to 4 domain requests. The 5th join will be a user. + application1 = completed_application(submitter=contact, name="city1.gov") + application2 = completed_application(submitter=contact, name="city2.gov") + application3 = completed_application(submitter=contact, name="city3.gov") + application4 = completed_application(submitter=contact, name="city4.gov") + + with patch("django.contrib.messages.warning") as mock_warning: + # Use the test client to simulate the request + 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 5th join will be a user. + mock_warning.assert_called_once_with( + response.wsgi_request, + "", + ) + + 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. + 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) + # Create an instance of the model + # join it to 5 domain requests. The 6th join will be a user. + contact, _ = Contact.objects.get_or_create(user=self.staffuser) + application1 = completed_application(submitter=contact, name="city1.gov") + application2 = completed_application(submitter=contact, name="city2.gov") + application3 = completed_application(submitter=contact, name="city3.gov") + application4 = completed_application(submitter=contact, name="city4.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 + response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk])) + logger.debug(mock_warning) + # Assert that the error message was called with the correct argument + # Note: The 6th join will be a user. + mock_warning.assert_called_once_with( + response.wsgi_request, + "" + "

        And 1 more...

        ", + ) + def tearDown(self): + DomainApplication.objects.all().delete() + Contact.objects.all().delete() User.objects.all().delete() -class VeryImportantPersonAdminTestCase(TestCase): +class VerifiedByStaffAdminTestCase(TestCase): def setUp(self): self.superuser = create_superuser() self.factory = RequestFactory() @@ -1794,13 +1820,13 @@ class VeryImportantPersonAdminTestCase(TestCase): self.client.force_login(self.superuser) # Create an instance of the admin class - admin_instance = VeryImportantPersonAdmin(model=VeryImportantPerson, admin_site=None) + admin_instance = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=None) - # Create a VeryImportantPerson instance - vip_instance = VeryImportantPerson(email="test@example.com", notes="Test Notes") + # Create a VerifiedByStaff instance + vip_instance = VerifiedByStaff(email="test@example.com", notes="Test Notes") # Create a request object - request = self.factory.post("/admin/yourapp/veryimportantperson/add/") + request = self.factory.post("/admin/yourapp/VerifiedByStaff/add/") request.user = self.superuser # Call the save_model method diff --git a/src/registrar/tests/test_emails.py b/src/registrar/tests/test_emails.py index a4f32bfcf..f2a94a186 100644 --- a/src/registrar/tests/test_emails.py +++ b/src/registrar/tests/test_emails.py @@ -102,9 +102,9 @@ class TestEmails(TestCase): application.submit() _, kwargs = self.mock_client.send_email.call_args body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] - self.assertNotIn("Other employees from your organization:", body) # spacing should be right between adjacent elements - self.assertRegex(body, r"5556\n\nAnything else") + self.assertRegex(body, r"5556\n\nOther employees") + self.assertRegex(body, r"None\n\nAnything else") @boto3_mocking.patching def test_submission_confirmation_alternative_govdomain_spacing(self): @@ -117,7 +117,7 @@ class TestEmails(TestCase): body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] self.assertIn("city1.gov", body) # spacing should be right between adjacent elements - self.assertRegex(body, r"city.gov\ncity1.gov\n\nPurpose of your domain:") + self.assertRegex(body, r"city.gov\n\nAlternative domains:\ncity1.gov\n\nPurpose of your domain:") @boto3_mocking.patching def test_submission_confirmation_no_alternative_govdomain_spacing(self): diff --git a/src/registrar/tests/test_environment_variables_effects.py b/src/registrar/tests/test_environment_variables_effects.py new file mode 100644 index 000000000..3a838c2a2 --- /dev/null +++ b/src/registrar/tests/test_environment_variables_effects.py @@ -0,0 +1,31 @@ +from django.test import Client, TestCase, override_settings +from django.contrib.auth import get_user_model + + +class MyTestCase(TestCase): + def setUp(self): + self.client = Client() + username = "test_user" + first_name = "First" + last_name = "Last" + email = "info@example.com" + self.user = get_user_model().objects.create( + username=username, first_name=first_name, last_name=last_name, email=email + ) + self.client.force_login(self.user) + + def tearDown(self): + super().tearDown() + self.user.delete() + + @override_settings(IS_PRODUCTION=True) + def test_production_environment(self): + """No banner on prod.""" + home_page = self.client.get("/") + self.assertNotContains(home_page, "You are on a test site.") + + @override_settings(IS_PRODUCTION=False) + def test_non_production_environment(self): + """Banner on non-prod.""" + home_page = self.client.get("/") + self.assertContains(home_page, "You are on a test site.") diff --git a/src/registrar/tests/test_forms.py b/src/registrar/tests/test_forms.py index a8d84ba7b..c9dd3c4f0 100644 --- a/src/registrar/tests/test_forms.py +++ b/src/registrar/tests/test_forms.py @@ -64,6 +64,12 @@ class TestFormValidation(MockEppLib): form = DotGovDomainForm(data={"requested_domain": "top-level-agency"}) self.assertEqual(len(form.errors), 0) + def test_requested_domain_starting_www(self): + """Test a valid domain name with .www at the beginning.""" + form = DotGovDomainForm(data={"requested_domain": "www.top-level-agency"}) + self.assertEqual(len(form.errors), 0) + self.assertEqual(form.cleaned_data["requested_domain"], "top-level-agency") + def test_requested_domain_ending_dotgov(self): """Just a valid domain name with .gov at the end.""" form = DotGovDomainForm(data={"requested_domain": "top-level-agency.gov"}) diff --git a/src/registrar/tests/test_management_scripts.py b/src/registrar/tests/test_management_scripts.py index 06886ba66..34178e262 100644 --- a/src/registrar/tests/test_management_scripts.py +++ b/src/registrar/tests/test_management_scripts.py @@ -1,5 +1,6 @@ import copy -import datetime +from datetime import date, datetime, time +from django.utils import timezone from django.test import TestCase @@ -17,7 +18,7 @@ from django.core.management import call_command from unittest.mock import patch, call from epplibwrapper import commands, common -from .common import MockEppLib +from .common import MockEppLib, less_console_noise 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) # 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): """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 execute the populate_first_ready command with the specified arguments. """ - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command("populate_first_ready") + with less_console_noise(): + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + call_command("populate_first_ready") def test_populate_first_ready_state_ready(self): """ Tests that the populate_first_ready works as expected for the state 'ready' """ - # Set the created at date - self.ready_domain.created_at = self.ready_at_date - self.ready_domain.save() - - desired_domain = copy.deepcopy(self.ready_domain) - - desired_domain.first_ready = self.ready_at_date - - # Run the expiration date script - self.run_populate_first_ready() - - self.assertEqual(desired_domain, self.ready_domain) - - # 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) + with less_console_noise(): + # Set the created at date + self.ready_domain.created_at = self.ready_at_date_tz_aware + self.ready_domain.save() + desired_domain = copy.deepcopy(self.ready_domain) + desired_domain.first_ready = self.ready_at_date + # Run the expiration date script + self.run_populate_first_ready() + self.assertEqual(desired_domain, self.ready_domain) + # 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): """ Tests that the populate_first_ready works as expected for the state 'deleted' """ - # Set the created at date - self.deleted_domain.created_at = self.ready_at_date - self.deleted_domain.save() - - desired_domain = copy.deepcopy(self.deleted_domain) - - desired_domain.first_ready = self.ready_at_date - - # Run the expiration date script - self.run_populate_first_ready() - - self.assertEqual(desired_domain, self.deleted_domain) - - # 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) + with less_console_noise(): + # Set the created at date + self.deleted_domain.created_at = self.ready_at_date_tz_aware + self.deleted_domain.save() + desired_domain = copy.deepcopy(self.deleted_domain) + desired_domain.first_ready = self.ready_at_date + # Run the expiration date script + self.run_populate_first_ready() + self.assertEqual(desired_domain, self.deleted_domain) + # 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): """ Tests that the populate_first_ready doesn't make changes when a domain's state is 'dns_needed' """ - # Set the created at date - self.dns_needed_domain.created_at = self.ready_at_date - self.dns_needed_domain.save() - - desired_domain = copy.deepcopy(self.dns_needed_domain) - - desired_domain.first_ready = None - - # Run the expiration date script - self.run_populate_first_ready() - - current_domain = self.dns_needed_domain - # The object should largely be unaltered (does not test first_ready) - self.assertEqual(desired_domain, current_domain) - - 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) + with less_console_noise(): + # Set the created at date + 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.first_ready = None + # Run the expiration date script + self.run_populate_first_ready() + current_domain = self.dns_needed_domain + # The object should largely be unaltered (does not test first_ready) + self.assertEqual(desired_domain, current_domain) + 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): """ Tests that the populate_first_ready works as expected for the state 'on_hold' """ - self.hold_domain.created_at = self.ready_at_date - self.hold_domain.save() - - desired_domain = copy.deepcopy(self.hold_domain) - desired_domain.first_ready = self.ready_at_date - - # Run the update first ready_at script - self.run_populate_first_ready() - - current_domain = self.hold_domain - self.assertEqual(desired_domain, current_domain) - - # 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) + with less_console_noise(): + self.hold_domain.created_at = self.ready_at_date_tz_aware + self.hold_domain.save() + desired_domain = copy.deepcopy(self.hold_domain) + desired_domain.first_ready = self.ready_at_date + # Run the update first ready_at script + self.run_populate_first_ready() + current_domain = self.hold_domain + self.assertEqual(desired_domain, current_domain) + # 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): """ Tests that the populate_first_ready works as expected for the state 'unknown' """ - # Set the created at date - self.unknown_domain.created_at = self.ready_at_date - self.unknown_domain.save() - - desired_domain = copy.deepcopy(self.unknown_domain) - desired_domain.first_ready = None - - # Run the expiration date script - self.run_populate_first_ready() - - current_domain = self.unknown_domain - - # The object should largely be unaltered (does not test first_ready) - self.assertEqual(desired_domain, current_domain) - - # 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) + with less_console_noise(): + # Set the created at date + self.unknown_domain.created_at = self.ready_at_date_tz_aware + self.unknown_domain.save() + desired_domain = copy.deepcopy(self.unknown_domain) + desired_domain.first_ready = None + # Run the expiration date script + self.run_populate_first_ready() + current_domain = self.unknown_domain + # The object should largely be unaltered (does not test first_ready) + self.assertEqual(desired_domain, current_domain) + # 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): @@ -185,7 +169,8 @@ class TestPatchAgencyInfo(TestCase): @patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", return_value=True) def call_patch_federal_agency_info(self, mock_prompt): """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): """ @@ -194,17 +179,14 @@ class TestPatchAgencyInfo(TestCase): of a `DomainInformation` object when the corresponding `TransitionDomain` object has a valid `federal_agency`. """ - - # Ensure that the federal_agency is None - self.assertEqual(self.domain_info.federal_agency, None) - - self.call_patch_federal_agency_info() - - # Reload the domain_info object from the database - self.domain_info.refresh_from_db() - - # Check that the federal_agency field was updated - self.assertEqual(self.domain_info.federal_agency, "test agency") + with less_console_noise(): + # Ensure that the federal_agency is None + self.assertEqual(self.domain_info.federal_agency, None) + self.call_patch_federal_agency_info() + # Reload the domain_info object from the database + self.domain_info.refresh_from_db() + # Check that the federal_agency field was updated + self.assertEqual(self.domain_info.federal_agency, "test agency") def test_patch_agency_info_skip(self): """ @@ -213,21 +195,18 @@ class TestPatchAgencyInfo(TestCase): of a `DomainInformation` object when the corresponding `TransitionDomain` object does not exist. """ - # Set federal_agency to None to simulate a skip - self.transition_domain.federal_agency = None - self.transition_domain.save() - - with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context: - self.call_patch_federal_agency_info() - - # Check that the correct log message was output - self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0]) - - # Reload the domain_info object from the database - self.domain_info.refresh_from_db() - - # Check that the federal_agency field was not updated - self.assertIsNone(self.domain_info.federal_agency) + with less_console_noise(): + # Set federal_agency to None to simulate a skip + self.transition_domain.federal_agency = None + self.transition_domain.save() + with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context: + self.call_patch_federal_agency_info() + # Check that the correct log message was output + self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0]) + # Reload the domain_info object from the database + self.domain_info.refresh_from_db() + # 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): """ @@ -235,25 +214,21 @@ class TestPatchAgencyInfo(TestCase): updates the DomainInformation object, because a record exists in the provided current-full.csv file. """ - # Set federal_agency to None to simulate a skip - self.transition_domain.federal_agency = None - self.transition_domain.save() - - # Change the domain name to something parsable in the .csv - self.domain.name = "cdomain1.gov" - self.domain.save() - - with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context: - self.call_patch_federal_agency_info() - - # Check that the correct log message was output - self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0]) - - # Reload the domain_info object from the database - 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") + with less_console_noise(): + # Set federal_agency to None to simulate a skip + self.transition_domain.federal_agency = None + self.transition_domain.save() + # Change the domain name to something parsable in the .csv + self.domain.name = "cdomain1.gov" + self.domain.save() + with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context: + self.call_patch_federal_agency_info() + # Check that the correct log message was output + self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0]) + # Reload the domain_info object from the database + 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): """ @@ -261,20 +236,17 @@ class TestPatchAgencyInfo(TestCase): does not update the `federal_agency` field of a `DomainInformation` object """ - self.domain_info.federal_agency = "unchanged" - self.domain_info.save() - - with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="INFO") as context: - self.call_patch_federal_agency_info() - - # Check that the correct log message was output - self.assertIn("FINISHED", context.output[1]) - - # Reload the domain_info object from the database - self.domain_info.refresh_from_db() - - # Check that the federal_agency field was not updated - self.assertEqual(self.domain_info.federal_agency, "unchanged") + with less_console_noise(): + self.domain_info.federal_agency = "unchanged" + self.domain_info.save() + with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="INFO") as context: + self.call_patch_federal_agency_info() + # Check that the correct log message was output + self.assertIn("FINISHED", context.output[1]) + # Reload the domain_info object from the database + self.domain_info.refresh_from_db() + # Check that the federal_agency field was not updated + self.assertEqual(self.domain_info.federal_agency, "unchanged") class TestExtendExpirationDates(MockEppLib): @@ -283,39 +255,37 @@ class TestExtendExpirationDates(MockEppLib): super().setUp() # Create a valid domain that is updatable Domain.objects.get_or_create( - name="waterbutpurple.gov", state=Domain.State.READY, expiration_date=datetime.date(2023, 11, 15) + name="waterbutpurple.gov", state=Domain.State.READY, expiration_date=date(2023, 11, 15) ) TransitionDomain.objects.get_or_create( username="testytester@mail.com", domain_name="waterbutpurple.gov", - epp_expiration_date=datetime.date(2023, 11, 15), + epp_expiration_date=date(2023, 11, 15), ) # Create a domain with an invalid expiration date - Domain.objects.get_or_create( - name="fake.gov", state=Domain.State.READY, expiration_date=datetime.date(2022, 5, 25) - ) + Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY, expiration_date=date(2022, 5, 25)) TransitionDomain.objects.get_or_create( username="themoonisactuallycheese@mail.com", domain_name="fake.gov", - epp_expiration_date=datetime.date(2022, 5, 25), + epp_expiration_date=date(2022, 5, 25), ) # Create a domain with an invalid state Domain.objects.get_or_create( - name="fakeneeded.gov", state=Domain.State.DNS_NEEDED, expiration_date=datetime.date(2023, 11, 15) + name="fakeneeded.gov", state=Domain.State.DNS_NEEDED, expiration_date=date(2023, 11, 15) ) TransitionDomain.objects.get_or_create( username="fakeneeded@mail.com", domain_name="fakeneeded.gov", - epp_expiration_date=datetime.date(2023, 11, 15), + epp_expiration_date=date(2023, 11, 15), ) # Create a domain with a date greater than the maximum Domain.objects.get_or_create( - name="fakemaximum.gov", state=Domain.State.READY, expiration_date=datetime.date(2024, 12, 31) + name="fakemaximum.gov", state=Domain.State.READY, expiration_date=date(2024, 12, 31) ) TransitionDomain.objects.get_or_create( username="fakemaximum@mail.com", domain_name="fakemaximum.gov", - epp_expiration_date=datetime.date(2024, 12, 31), + epp_expiration_date=date(2024, 12, 31), ) def tearDown(self): @@ -338,83 +308,82 @@ class TestExtendExpirationDates(MockEppLib): The 'call_command' function from Django's management framework is then used to execute the extend_expiration_dates command with the specified arguments. """ - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command("extend_expiration_dates") + with less_console_noise(): + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + call_command("extend_expiration_dates") def test_extends_expiration_date_correctly(self): """ Tests that the extend_expiration_dates method extends dates as expected """ - desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get() - desired_domain.expiration_date = datetime.date(2024, 11, 15) - - # Run the expiration date script - self.run_extend_expiration_dates() - - current_domain = Domain.objects.filter(name="waterbutpurple.gov").get() - - self.assertEqual(desired_domain, current_domain) - # Explicitly test the expiration date - self.assertEqual(current_domain.expiration_date, datetime.date(2024, 11, 15)) + with less_console_noise(): + desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get() + desired_domain.expiration_date = date(2024, 11, 15) + # Run the expiration date script + self.run_extend_expiration_dates() + current_domain = Domain.objects.filter(name="waterbutpurple.gov").get() + self.assertEqual(desired_domain, current_domain) + # Explicitly test the expiration date + self.assertEqual(current_domain.expiration_date, date(2024, 11, 15)) def test_extends_expiration_date_skips_non_current(self): """ Tests that the extend_expiration_dates method correctly skips domains with an expiration date less than a certain threshold. """ - desired_domain = Domain.objects.filter(name="fake.gov").get() - desired_domain.expiration_date = datetime.date(2022, 5, 25) - - # Run the expiration date script - self.run_extend_expiration_dates() - - current_domain = Domain.objects.filter(name="fake.gov").get() - self.assertEqual(desired_domain, current_domain) - - # Explicitly test the expiration date. The extend_expiration_dates script - # will skip all dates less than date(2023, 11, 15), meaning that this domain - # should not be affected by the change. - self.assertEqual(current_domain.expiration_date, datetime.date(2022, 5, 25)) + with less_console_noise(): + desired_domain = Domain.objects.filter(name="fake.gov").get() + desired_domain.expiration_date = date(2022, 5, 25) + # Run the expiration date script + self.run_extend_expiration_dates() + current_domain = Domain.objects.filter(name="fake.gov").get() + self.assertEqual(desired_domain, current_domain) + # Explicitly test the expiration date. The extend_expiration_dates script + # will skip all dates less than date(2023, 11, 15), meaning that this domain + # should not be affected by the change. + self.assertEqual(current_domain.expiration_date, date(2022, 5, 25)) def test_extends_expiration_date_skips_maximum_date(self): """ Tests that the extend_expiration_dates method correctly skips domains with an expiration date more than a certain threshold. """ - desired_domain = Domain.objects.filter(name="fakemaximum.gov").get() - desired_domain.expiration_date = datetime.date(2024, 12, 31) + with less_console_noise(): + desired_domain = Domain.objects.filter(name="fakemaximum.gov").get() + desired_domain.expiration_date = date(2024, 12, 31) - # Run the expiration date script - self.run_extend_expiration_dates() + # Run the expiration date script + self.run_extend_expiration_dates() - current_domain = Domain.objects.filter(name="fakemaximum.gov").get() - self.assertEqual(desired_domain, current_domain) + current_domain = Domain.objects.filter(name="fakemaximum.gov").get() + self.assertEqual(desired_domain, current_domain) - # Explicitly test the expiration date. The extend_expiration_dates script - # will skip all dates less than date(2023, 11, 15), meaning that this domain - # should not be affected by the change. - self.assertEqual(current_domain.expiration_date, datetime.date(2024, 12, 31)) + # Explicitly test the expiration date. The extend_expiration_dates script + # will skip all dates less than date(2023, 11, 15), meaning that this domain + # should not be affected by the change. + self.assertEqual(current_domain.expiration_date, date(2024, 12, 31)) def test_extends_expiration_date_skips_non_ready(self): """ Tests that the extend_expiration_dates method correctly skips domains not in the state "ready" """ - desired_domain = Domain.objects.filter(name="fakeneeded.gov").get() - desired_domain.expiration_date = datetime.date(2023, 11, 15) + with less_console_noise(): + desired_domain = Domain.objects.filter(name="fakeneeded.gov").get() + desired_domain.expiration_date = date(2023, 11, 15) - # Run the expiration date script - self.run_extend_expiration_dates() + # Run the expiration date script + self.run_extend_expiration_dates() - current_domain = Domain.objects.filter(name="fakeneeded.gov").get() - self.assertEqual(desired_domain, current_domain) + current_domain = Domain.objects.filter(name="fakeneeded.gov").get() + self.assertEqual(desired_domain, current_domain) - # Explicitly test the expiration date. The extend_expiration_dates script - # will skip all dates less than date(2023, 11, 15), meaning that this domain - # should not be affected by the change. - self.assertEqual(current_domain.expiration_date, datetime.date(2023, 11, 15)) + # Explicitly test the expiration date. The extend_expiration_dates script + # will skip all dates less than date(2023, 11, 15), meaning that this domain + # should not be affected by the change. + self.assertEqual(current_domain.expiration_date, date(2023, 11, 15)) 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 of a domain beyond the initial extension. """ - desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get() - desired_domain.expiration_date = datetime.date(2024, 11, 15) - - # Run the expiration date script - self.run_extend_expiration_dates() - - current_domain = Domain.objects.filter(name="waterbutpurple.gov").get() - self.assertEqual(desired_domain, current_domain) - - # Explicitly test the expiration date - self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15)) - - # Run the expiration date script again - self.run_extend_expiration_dates() - - # The old domain shouldn't have changed - self.assertEqual(desired_domain, current_domain) - - # Explicitly test the expiration date - should be the same - self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15)) + with less_console_noise(): + desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get() + desired_domain.expiration_date = date(2024, 11, 15) + # Run the expiration date script + self.run_extend_expiration_dates() + current_domain = Domain.objects.filter(name="waterbutpurple.gov").get() + self.assertEqual(desired_domain, current_domain) + # Explicitly test the expiration date + self.assertEqual(desired_domain.expiration_date, date(2024, 11, 15)) + # Run the expiration date script again + self.run_extend_expiration_dates() + # The old domain shouldn't have changed + self.assertEqual(desired_domain, current_domain) + # Explicitly test the expiration date - should be the same + self.assertEqual(desired_domain.expiration_date, date(2024, 11, 15)) class TestDiscloseEmails(MockEppLib): @@ -461,39 +425,41 @@ class TestDiscloseEmails(MockEppLib): The 'call_command' function from Django's management framework is then used to execute the disclose_security_emails command. """ - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command("disclose_security_emails") + with less_console_noise(): + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + call_command("disclose_security_emails") def test_disclose_security_emails(self): """ Tests that command disclose_security_emails runs successfully with appropriate EPP calll to UpdateContact. """ - domain, _ = Domain.objects.get_or_create(name="testdisclose.gov", state=Domain.State.READY) - expectedSecContact = PublicContact.get_default_security() - expectedSecContact.domain = domain - expectedSecContact.email = "123@mail.gov" - # set domain security email to 123@mail.gov instead of default email - domain.security_contact = expectedSecContact - self.run_disclose_security_emails() + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="testdisclose.gov", state=Domain.State.READY) + expectedSecContact = PublicContact.get_default_security() + expectedSecContact.domain = domain + expectedSecContact.email = "123@mail.gov" + # set domain security email to 123@mail.gov instead of default email + domain.security_contact = expectedSecContact + self.run_disclose_security_emails() - # running disclose_security_emails sends EPP call UpdateContact with disclose - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.UpdateContact( - id=domain.security_contact.registry_id, - postal_info=domain._make_epp_contact_postal_info(contact=domain.security_contact), - email=domain.security_contact.email, - voice=domain.security_contact.voice, - fax=domain.security_contact.fax, - auth_info=common.ContactAuthInfo(pw="2fooBAR123fooBaz"), - disclose=domain._disclose_fields(contact=domain.security_contact), - ), - cleaned=True, - ) - ] - ) + # running disclose_security_emails sends EPP call UpdateContact with disclose + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.UpdateContact( + id=domain.security_contact.registry_id, + postal_info=domain._make_epp_contact_postal_info(contact=domain.security_contact), + email=domain.security_contact.email, + voice=domain.security_contact.voice, + fax=domain.security_contact.fax, + auth_info=common.ContactAuthInfo(pw="2fooBAR123fooBaz"), + disclose=domain._disclose_fields(contact=domain.security_contact), + ), + cleaned=True, + ) + ] + ) diff --git a/src/registrar/tests/test_migrations.py b/src/registrar/tests/test_migrations.py index cc9d379e5..773a885c1 100644 --- a/src/registrar/tests/test_migrations.py +++ b/src/registrar/tests/test_migrations.py @@ -43,6 +43,9 @@ class TestGroups(TestCase): "change_user", "delete_userdomainrole", "view_userdomainrole", + "add_verifiedbystaff", + "change_verifiedbystaff", + "delete_verifiedbystaff", "change_website", ] diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 66c8d04f8..d0782c359 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -16,7 +16,7 @@ from registrar.models import ( import boto3_mocking from registrar.models.transition_domain import TransitionDomain -from registrar.models.very_important_person import VeryImportantPerson # type: ignore +from registrar.models.verified_by_staff import VerifiedByStaff # type: ignore from .common import MockSESClient, less_console_noise, completed_application from django_fsm import TransitionNotAllowed @@ -60,99 +60,106 @@ class TestDomainApplication(TestCase): def assertNotRaises(self, exception_type): """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): """Can't create a completely empty domain application. NOTE: something about theexception this test raises messes up with the atomic block in a custom tearDown method for the parent test class.""" - with self.assertRaisesRegex(IntegrityError, "creator"): - DomainApplication.objects.create() + with less_console_noise(): + with self.assertRaisesRegex(IntegrityError, "creator"): + DomainApplication.objects.create() def test_minimal_create(self): """Can create with just a creator.""" - user, _ = User.objects.get_or_create(username="testy") - application = DomainApplication.objects.create(creator=user) - self.assertEqual(application.status, DomainApplication.ApplicationStatus.STARTED) + with less_console_noise(): + user, _ = User.objects.get_or_create(username="testy") + application = DomainApplication.objects.create(creator=user) + self.assertEqual(application.status, DomainApplication.ApplicationStatus.STARTED) def test_full_create(self): """Can create with all fields.""" - user, _ = User.objects.get_or_create(username="testy") - contact = Contact.objects.create() - com_website, _ = Website.objects.get_or_create(website="igorville.com") - gov_website, _ = Website.objects.get_or_create(website="igorville.gov") - domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") - application = DomainApplication.objects.create( - creator=user, - investigator=user, - organization_type=DomainApplication.OrganizationChoices.FEDERAL, - federal_type=DomainApplication.BranchChoices.EXECUTIVE, - is_election_board=False, - organization_name="Test", - address_line1="100 Main St.", - address_line2="APT 1A", - state_territory="CA", - zipcode="12345-6789", - authorizing_official=contact, - requested_domain=domain, - submitter=contact, - purpose="Igorville rules!", - 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.other_contacts.add(contact) - application.save() + with less_console_noise(): + user, _ = User.objects.get_or_create(username="testy") + contact = Contact.objects.create() + com_website, _ = Website.objects.get_or_create(website="igorville.com") + gov_website, _ = Website.objects.get_or_create(website="igorville.gov") + domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") + application = DomainApplication.objects.create( + creator=user, + investigator=user, + organization_type=DomainApplication.OrganizationChoices.FEDERAL, + federal_type=DomainApplication.BranchChoices.EXECUTIVE, + is_election_board=False, + organization_name="Test", + address_line1="100 Main St.", + address_line2="APT 1A", + state_territory="CA", + zipcode="12345-6789", + authorizing_official=contact, + requested_domain=domain, + submitter=contact, + purpose="Igorville rules!", + 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.other_contacts.add(contact) + application.save() def test_domain_info(self): """Can create domain info with all fields.""" - user, _ = User.objects.get_or_create(username="testy") - contact = Contact.objects.create() - domain, _ = Domain.objects.get_or_create(name="igorville.gov") - information = DomainInformation.objects.create( - creator=user, - organization_type=DomainInformation.OrganizationChoices.FEDERAL, - federal_type=DomainInformation.BranchChoices.EXECUTIVE, - is_election_board=False, - organization_name="Test", - address_line1="100 Main St.", - address_line2="APT 1A", - state_territory="CA", - zipcode="12345-6789", - authorizing_official=contact, - submitter=contact, - purpose="Igorville rules!", - anything_else="All of Igorville loves the dotgov program.", - is_policy_acknowledged=True, - domain=domain, - ) - information.other_contacts.add(contact) - information.save() - self.assertEqual(information.domain.id, domain.id) - self.assertEqual(information.id, domain.domain_info.id) + with less_console_noise(): + user, _ = User.objects.get_or_create(username="testy") + contact = Contact.objects.create() + domain, _ = Domain.objects.get_or_create(name="igorville.gov") + information = DomainInformation.objects.create( + creator=user, + organization_type=DomainInformation.OrganizationChoices.FEDERAL, + federal_type=DomainInformation.BranchChoices.EXECUTIVE, + is_election_board=False, + organization_name="Test", + address_line1="100 Main St.", + address_line2="APT 1A", + state_territory="CA", + zipcode="12345-6789", + authorizing_official=contact, + submitter=contact, + purpose="Igorville rules!", + anything_else="All of Igorville loves the dotgov program.", + is_policy_acknowledged=True, + domain=domain, + ) + information.other_contacts.add(contact) + information.save() + self.assertEqual(information.domain.id, domain.id) + self.assertEqual(information.id, domain.domain_info.id) def test_status_fsm_submit_fail(self): - user, _ = User.objects.get_or_create(username="testy") - application = DomainApplication.objects.create(creator=user) + with less_console_noise(): + 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 less_console_noise(): - with self.assertRaises(ValueError): - # can't submit an application with a null domain name - application.submit() + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + with self.assertRaises(ValueError): + # can't submit an application with a null domain name + application.submit() def test_status_fsm_submit_succeed(self): - user, _ = User.objects.get_or_create(username="testy") - site = DraftDomain.objects.create(name="igorville.gov") - application = DomainApplication.objects.create(creator=user, requested_domain=site) + with less_console_noise(): + user, _ = User.objects.get_or_create(username="testy") + 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 less_console_noise(): - application.submit() - self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED) + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + application.submit() + self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED) def test_submit_from_started_sends_email(self): """Create an application and submit it and see if email was sent.""" @@ -160,8 +167,7 @@ class TestDomainApplication(TestCase): # submitter's email is mayor@igorville.gov application = completed_application() - with boto3_mocking.clients.handler_for("sesv2", self.mock_client): - with less_console_noise(): + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): application.submit() # check to see if an email was sent @@ -394,13 +400,13 @@ class TestDomainApplication(TestCase): (self.rejected_application, TransitionNotAllowed), (self.ineligible_application, TransitionNotAllowed), ] - - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - try: - application.action_needed() - except TransitionNotAllowed: - self.fail("TransitionNotAllowed was raised, but it was not expected.") + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + try: + application.action_needed() + except TransitionNotAllowed: + self.fail("TransitionNotAllowed was raised, but it was not expected.") def test_action_needed_transition_not_allowed(self): """ @@ -412,11 +418,11 @@ class TestDomainApplication(TestCase): (self.action_needed_application, TransitionNotAllowed), (self.withdrawn_application, TransitionNotAllowed), ] - - for application, exception_type in test_cases: - with self.subTest(application=application, exception_type=exception_type): - with self.assertRaises(exception_type): - application.action_needed() + with less_console_noise(): + for application, exception_type in test_cases: + with self.subTest(application=application, exception_type=exception_type): + with self.assertRaises(exception_type): + application.action_needed() def test_approved_transition_allowed(self): """ @@ -583,6 +589,46 @@ class TestDomainApplication(TestCase): with self.assertRaises(exception_type): application.reject_with_prejudice() + def test_transition_not_allowed_approved_in_review_when_domain_is_active(self): + """Create an application with status approved, create a matching domain that + is active, and call in_review against transition rules""" + + domain = Domain.objects.create(name=self.approved_application.requested_domain.name) + self.approved_application.approved_domain = domain + self.approved_application.save() + + # Define a custom implementation for is_active + def custom_is_active(self): + return True # Override to return True + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Use patch to temporarily replace is_active with the custom implementation + with patch.object(Domain, "is_active", custom_is_active): + # Now, when you call is_active on Domain, it will return True + with self.assertRaises(TransitionNotAllowed): + self.approved_application.in_review() + + def test_transition_not_allowed_approved_action_needed_when_domain_is_active(self): + """Create an application with status approved, create a matching domain that + is active, and call action_needed against transition rules""" + + domain = Domain.objects.create(name=self.approved_application.requested_domain.name) + self.approved_application.approved_domain = domain + self.approved_application.save() + + # Define a custom implementation for is_active + def custom_is_active(self): + return True # Override to return True + + with boto3_mocking.clients.handler_for("sesv2", self.mock_client): + with less_console_noise(): + # Use patch to temporarily replace is_active with the custom implementation + with patch.object(Domain, "is_active", custom_is_active): + # Now, when you call is_active on Domain, it will return True + with self.assertRaises(TransitionNotAllowed): + self.approved_application.action_needed() + def test_transition_not_allowed_approved_rejected_when_domain_is_active(self): """Create an application with status approved, create a matching domain that is active, and call reject against transition rules""" @@ -625,25 +671,29 @@ class TestDomainApplication(TestCase): def test_has_rationale_returns_true(self): """has_rationale() returns true when an application has no_other_contacts_rationale""" - self.started_application.no_other_contacts_rationale = "You talkin' to me?" - self.started_application.save() - self.assertEquals(self.started_application.has_rationale(), True) + with less_console_noise(): + self.started_application.no_other_contacts_rationale = "You talkin' to me?" + self.started_application.save() + self.assertEquals(self.started_application.has_rationale(), True) def test_has_rationale_returns_false(self): """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): """has_other_contacts() returns true when an application has other_contacts""" - # completed_application has other contacts by default - self.assertEquals(self.started_application.has_other_contacts(), True) + with less_console_noise(): + # completed_application has other contacts by default + self.assertEquals(self.started_application.has_other_contacts(), True) def test_has_other_contacts_returns_false(self): """has_other_contacts() returns false when an application has no other_contacts""" - application = completed_application( - status=DomainApplication.ApplicationStatus.STARTED, name="no-others.gov", has_other_contacts=False - ) - self.assertEquals(application.has_other_contacts(), False) + with less_console_noise(): + application = completed_application( + status=DomainApplication.ApplicationStatus.STARTED, name="no-others.gov", has_other_contacts=False + ) + self.assertEquals(application.has_other_contacts(), False) class TestPermissions(TestCase): @@ -674,9 +724,9 @@ class TestPermissions(TestCase): 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): super().setUp() @@ -685,12 +735,18 @@ class TestDomainInfo(TestCase): def tearDown(self): super().tearDown() 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 def test_approval_creates_info(self): + self.maxDiff = None draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") 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 less_console_noise(): @@ -700,7 +756,25 @@ class TestDomainInfo(TestCase): # should be an information present for this domain 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): @@ -782,7 +856,7 @@ class TestUser(TestCase): def test_identity_verification_with_very_important_person(self): """A Very Important Person should return False when tested with class method needs_identity_verification""" - VeryImportantPerson.objects.get_or_create(email=self.user.email) + VerifiedByStaff.objects.get_or_create(email=self.user.email) self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username)) def test_identity_verification_with_invited_user(self): diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 9026832cd..ca0a5e8d8 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -7,6 +7,7 @@ from django.test import TestCase from django.db.utils import IntegrityError from unittest.mock import MagicMock, patch, call import datetime +from django.utils.timezone import make_aware from registrar.models import Domain, Host, HostIP from unittest import skip @@ -46,158 +47,162 @@ class TestDomainCache(MockEppLib): def test_cache_sets_resets(self): """Cache should be set on getter and reset on setter calls""" - domain, _ = Domain.objects.get_or_create(name="igorville.gov") - # trigger getter - _ = domain.creation_date - domain._get_property("contacts") - # getter should set the domain cache with a InfoDomain object - # (see InfoDomainResult) - self.assertEquals(domain._cache["auth_info"], self.mockDataInfoDomain.auth_info) - self.assertEquals(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date) - status_list = [status.state for status in self.mockDataInfoDomain.statuses] - self.assertEquals(domain._cache["statuses"], status_list) - self.assertFalse("avail" in domain._cache.keys()) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="igorville.gov") + # trigger getter + _ = domain.creation_date + domain._get_property("contacts") + # getter should set the domain cache with a InfoDomain object + # (see InfoDomainResult) + self.assertEquals(domain._cache["auth_info"], self.mockDataInfoDomain.auth_info) + self.assertEquals(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date) + status_list = [status.state for status in self.mockDataInfoDomain.statuses] + self.assertEquals(domain._cache["statuses"], status_list) + self.assertFalse("avail" in domain._cache.keys()) - # using a setter should clear the cache - domain.dnssecdata = [] - self.assertEquals(domain._cache, {}) + # using a setter should clear the cache + domain.dnssecdata = [] + self.assertEquals(domain._cache, {}) - # send should have been called only once - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.InfoDomain(name="igorville.gov", auth_info=None), - cleaned=True, - ), - ], - any_order=False, # Ensure calls are in the specified order - ) + # send should have been called only once + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.InfoDomain(name="igorville.gov", auth_info=None), + cleaned=True, + ), + ], + any_order=False, # Ensure calls are in the specified order + ) def test_cache_used_when_avail(self): """Cache is pulled from if the object has already been accessed""" - domain, _ = Domain.objects.get_or_create(name="igorville.gov") - cr_date = domain.creation_date + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="igorville.gov") + cr_date = domain.creation_date - # repeat the getter call - cr_date = domain.creation_date + # repeat the getter call + cr_date = domain.creation_date - # value should still be set correctly - self.assertEqual(cr_date, self.mockDataInfoDomain.cr_date) - self.assertEqual(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date) + # value should still be set correctly + self.assertEqual(cr_date, self.mockDataInfoDomain.cr_date) + self.assertEqual(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date) - # send was only called once & not on the second getter call - expectedCalls = [ - call(commands.InfoDomain(name="igorville.gov", auth_info=None), cleaned=True), - ] + # send was only called once & not on the second getter call + expectedCalls = [ + call(commands.InfoDomain(name="igorville.gov", auth_info=None), cleaned=True), + ] - self.mockedSendFunction.assert_has_calls(expectedCalls) + self.mockedSendFunction.assert_has_calls(expectedCalls) def test_cache_nested_elements(self): """Cache works correctly with the nested objects cache and hosts""" - domain, _ = Domain.objects.get_or_create(name="igorville.gov") - # The contact list will initially contain objects of type 'DomainContact' - # this is then transformed into PublicContact, and cache should NOT - # hold onto the DomainContact object - expectedUnfurledContactsList = [ - common.DomainContact(contact="123", type="security"), - ] - expectedContactsDict = { - PublicContact.ContactTypeChoices.ADMINISTRATIVE: None, - PublicContact.ContactTypeChoices.SECURITY: "123", - PublicContact.ContactTypeChoices.TECHNICAL: None, - } - expectedHostsDict = { - "name": self.mockDataInfoDomain.hosts[0], - "addrs": [item.addr for item in self.mockDataInfoHosts.addrs], - "cr_date": self.mockDataInfoHosts.cr_date, - } + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="igorville.gov") + # The contact list will initially contain objects of type 'DomainContact' + # this is then transformed into PublicContact, and cache should NOT + # hold onto the DomainContact object + expectedUnfurledContactsList = [ + common.DomainContact(contact="123", type="security"), + ] + expectedContactsDict = { + PublicContact.ContactTypeChoices.ADMINISTRATIVE: None, + PublicContact.ContactTypeChoices.SECURITY: "123", + PublicContact.ContactTypeChoices.TECHNICAL: None, + } + expectedHostsDict = { + "name": self.mockDataInfoDomain.hosts[0], + "addrs": [item.addr for item in self.mockDataInfoHosts.addrs], + "cr_date": self.mockDataInfoHosts.cr_date, + } - # this can be changed when the getter for contacts is implemented - domain._get_property("contacts") + # this can be changed when the getter for contacts is implemented + domain._get_property("contacts") - # check domain info is still correct and not overridden - self.assertEqual(domain._cache["auth_info"], self.mockDataInfoDomain.auth_info) - self.assertEqual(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date) + # check domain info is still correct and not overridden + self.assertEqual(domain._cache["auth_info"], self.mockDataInfoDomain.auth_info) + self.assertEqual(domain._cache["cr_date"], self.mockDataInfoDomain.cr_date) - # check contacts - self.assertEqual(domain._cache["_contacts"], self.mockDataInfoDomain.contacts) - # The contact list should not contain what is sent by the registry by default, - # as _fetch_cache will transform the type to PublicContact - self.assertNotEqual(domain._cache["contacts"], expectedUnfurledContactsList) - self.assertEqual(domain._cache["contacts"], expectedContactsDict) + # check contacts + self.assertEqual(domain._cache["_contacts"], self.mockDataInfoDomain.contacts) + # The contact list should not contain what is sent by the registry by default, + # as _fetch_cache will transform the type to PublicContact + self.assertNotEqual(domain._cache["contacts"], expectedUnfurledContactsList) + self.assertEqual(domain._cache["contacts"], expectedContactsDict) - # get and check hosts is set correctly - domain._get_property("hosts") - self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) - self.assertEqual(domain._cache["contacts"], expectedContactsDict) - # invalidate cache - domain._cache = {} + # get and check hosts is set correctly + domain._get_property("hosts") + self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) + self.assertEqual(domain._cache["contacts"], expectedContactsDict) + # invalidate cache + domain._cache = {} - # get host - domain._get_property("hosts") - self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) + # get host + domain._get_property("hosts") + self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) - # get contacts - domain._get_property("contacts") - self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) - self.assertEqual(domain._cache["contacts"], expectedContactsDict) + # get contacts + domain._get_property("contacts") + self.assertEqual(domain._cache["hosts"], [expectedHostsDict]) + self.assertEqual(domain._cache["contacts"], expectedContactsDict) def test_map_epp_contact_to_public_contact(self): # Tests that the mapper is working how we expect - domain, _ = Domain.objects.get_or_create(name="registry.gov") - security = PublicContact.ContactTypeChoices.SECURITY - mapped = domain.map_epp_contact_to_public_contact( - self.mockDataInfoContact, - self.mockDataInfoContact.id, - security, - ) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="registry.gov") + security = PublicContact.ContactTypeChoices.SECURITY + mapped = domain.map_epp_contact_to_public_contact( + self.mockDataInfoContact, + self.mockDataInfoContact.id, + security, + ) - expected_contact = PublicContact( - domain=domain, - contact_type=security, - registry_id="123", - email="123@mail.gov", - voice="+1.8882820870", - fax="+1-212-9876543", - pw="lastPw", - name="Registry Customer Service", - org="Cybersecurity and Infrastructure Security Agency", - city="Arlington", - pc="22201", - cc="US", - sp="VA", - street1="4200 Wilson Blvd.", - ) + expected_contact = PublicContact( + domain=domain, + contact_type=security, + registry_id="123", + email="123@mail.gov", + voice="+1.8882820870", + fax="+1-212-9876543", + pw="lastPw", + name="Registry Customer Service", + org="Cybersecurity and Infrastructure Security Agency", + city="Arlington", + pc="22201", + cc="US", + sp="VA", + street1="4200 Wilson Blvd.", + ) - # Test purposes only, since we're comparing - # two duplicate objects. We would expect - # these not to have the same state. - expected_contact._state = mapped._state + # Test purposes only, since we're comparing + # two duplicate objects. We would expect + # these not to have the same state. + expected_contact._state = mapped._state - # Mapped object is what we expect - self.assertEqual(mapped.__dict__, expected_contact.__dict__) + # Mapped object is what we expect + self.assertEqual(mapped.__dict__, expected_contact.__dict__) - # The mapped object should correctly translate to a DB - # object. If not, something else went wrong. - db_object = domain._get_or_create_public_contact(mapped) - in_db = PublicContact.objects.filter( - registry_id=domain.security_contact.registry_id, - contact_type=security, - ).get() - # DB Object is the same as the mapped object - self.assertEqual(db_object, in_db) + # The mapped object should correctly translate to a DB + # object. If not, something else went wrong. + db_object = domain._get_or_create_public_contact(mapped) + in_db = PublicContact.objects.filter( + registry_id=domain.security_contact.registry_id, + contact_type=security, + ).get() + # DB Object is the same as the mapped object + self.assertEqual(db_object, in_db) - domain.security_contact = in_db - # Trigger the getter - _ = domain.security_contact - # Check to see that changes made - # to DB objects persist in cache correctly - in_db.email = "123test@mail.gov" - in_db.save() + domain.security_contact = in_db + # Trigger the getter + _ = domain.security_contact + # Check to see that changes made + # to DB objects persist in cache correctly + in_db.email = "123test@mail.gov" + in_db.save() - cached_contact = domain._cache["contacts"].get(security) - self.assertEqual(cached_contact, in_db.registry_id) - self.assertEqual(domain.security_contact.email, "123test@mail.gov") + cached_contact = domain._cache["contacts"].get(security) + self.assertEqual(cached_contact, in_db.registry_id) + self.assertEqual(domain.security_contact.email, "123test@mail.gov") def test_errors_map_epp_contact_to_public_contact(self): """ @@ -206,48 +211,49 @@ class TestDomainCache(MockEppLib): gets invalid data from EPPLib Then the function throws the expected ContactErrors """ - domain, _ = Domain.objects.get_or_create(name="registry.gov") - fakedEpp = self.fakedEppObject() - invalid_length = fakedEpp.dummyInfoContactResultData( - "Cymaticsisasubsetofmodalvibrationalphenomena", "lengthInvalid@mail.gov" - ) - valid_object = fakedEpp.dummyInfoContactResultData("valid", "valid@mail.gov") - - desired_error = ContactErrorCodes.CONTACT_ID_INVALID_LENGTH - with self.assertRaises(ContactError) as context: - domain.map_epp_contact_to_public_contact( - invalid_length, - invalid_length.id, - PublicContact.ContactTypeChoices.SECURITY, + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="registry.gov") + fakedEpp = self.fakedEppObject() + invalid_length = fakedEpp.dummyInfoContactResultData( + "Cymaticsisasubsetofmodalvibrationalphenomena", "lengthInvalid@mail.gov" ) - self.assertEqual(context.exception.code, desired_error) + valid_object = fakedEpp.dummyInfoContactResultData("valid", "valid@mail.gov") - desired_error = ContactErrorCodes.CONTACT_ID_NONE - with self.assertRaises(ContactError) as context: - domain.map_epp_contact_to_public_contact( - valid_object, - None, - PublicContact.ContactTypeChoices.SECURITY, - ) - self.assertEqual(context.exception.code, desired_error) + desired_error = ContactErrorCodes.CONTACT_ID_INVALID_LENGTH + with self.assertRaises(ContactError) as context: + domain.map_epp_contact_to_public_contact( + invalid_length, + invalid_length.id, + PublicContact.ContactTypeChoices.SECURITY, + ) + self.assertEqual(context.exception.code, desired_error) - desired_error = ContactErrorCodes.CONTACT_INVALID_TYPE - with self.assertRaises(ContactError) as context: - domain.map_epp_contact_to_public_contact( - "bad_object", - valid_object.id, - PublicContact.ContactTypeChoices.SECURITY, - ) - self.assertEqual(context.exception.code, desired_error) + desired_error = ContactErrorCodes.CONTACT_ID_NONE + with self.assertRaises(ContactError) as context: + domain.map_epp_contact_to_public_contact( + valid_object, + None, + PublicContact.ContactTypeChoices.SECURITY, + ) + self.assertEqual(context.exception.code, desired_error) - desired_error = ContactErrorCodes.CONTACT_TYPE_NONE - with self.assertRaises(ContactError) as context: - domain.map_epp_contact_to_public_contact( - valid_object, - valid_object.id, - None, - ) - self.assertEqual(context.exception.code, desired_error) + desired_error = ContactErrorCodes.CONTACT_INVALID_TYPE + with self.assertRaises(ContactError) as context: + domain.map_epp_contact_to_public_contact( + "bad_object", + valid_object.id, + PublicContact.ContactTypeChoices.SECURITY, + ) + self.assertEqual(context.exception.code, desired_error) + + desired_error = ContactErrorCodes.CONTACT_TYPE_NONE + with self.assertRaises(ContactError) as context: + domain.map_epp_contact_to_public_contact( + valid_object, + valid_object.id, + None, + ) + self.assertEqual(context.exception.code, desired_error) class TestDomainCreation(MockEppLib): @@ -346,42 +352,44 @@ class TestDomainStatuses(MockEppLib): def test_get_status(self): """Domain 'statuses' getter returns statuses by calling epp""" - domain, _ = Domain.objects.get_or_create(name="chicken-liver.gov") - # trigger getter - _ = domain.statuses - status_list = [status.state for status in self.mockDataInfoDomain.statuses] - self.assertEquals(domain._cache["statuses"], status_list) - # Called in _fetch_cache - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.InfoDomain(name="chicken-liver.gov", auth_info=None), - cleaned=True, - ), - ], - any_order=False, # Ensure calls are in the specified order - ) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="chicken-liver.gov") + # trigger getter + _ = domain.statuses + status_list = [status.state for status in self.mockDataInfoDomain.statuses] + self.assertEquals(domain._cache["statuses"], status_list) + # Called in _fetch_cache + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.InfoDomain(name="chicken-liver.gov", auth_info=None), + cleaned=True, + ), + ], + any_order=False, # Ensure calls are in the specified order + ) def test_get_status_returns_empty_list_when_value_error(self): """Domain 'statuses' getter returns an empty list when value error""" - domain, _ = Domain.objects.get_or_create(name="pig-knuckles.gov") + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="pig-knuckles.gov") - def side_effect(self): - raise KeyError + def side_effect(self): + raise KeyError - patcher = patch("registrar.models.domain.Domain._get_property") - mocked_get = patcher.start() - mocked_get.side_effect = side_effect + patcher = patch("registrar.models.domain.Domain._get_property") + mocked_get = patcher.start() + mocked_get.side_effect = side_effect - # trigger getter - _ = domain.statuses + # trigger getter + _ = domain.statuses - with self.assertRaises(KeyError): - _ = domain._cache["statuses"] - self.assertEquals(_, []) + with self.assertRaises(KeyError): + _ = domain._cache["statuses"] + self.assertEquals(_, []) - patcher.stop() + patcher.stop() @skip("not implemented yet") def test_place_client_hold_sets_status(self): @@ -398,28 +406,23 @@ class TestDomainStatuses(MockEppLib): first_ready is set when a domain is first transitioned to READY. It does not get overwritten in case the domain gets out of and back into READY. """ - domain, _ = Domain.objects.get_or_create(name="pig-knuckles.gov", state=Domain.State.DNS_NEEDED) - self.assertEqual(domain.first_ready, None) - - domain.ready() - - # check that status is READY - self.assertTrue(domain.is_active()) - self.assertNotEqual(domain.first_ready, None) - - # Capture the value of first_ready - first_ready = domain.first_ready - - # change domain status - domain.dns_needed() - self.assertFalse(domain.is_active()) - - # change back to READY - domain.ready() - self.assertTrue(domain.is_active()) - - # assert that the value of first_ready has not changed - self.assertEqual(domain.first_ready, first_ready) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="pig-knuckles.gov", state=Domain.State.DNS_NEEDED) + self.assertEqual(domain.first_ready, None) + domain.ready() + # check that status is READY + self.assertTrue(domain.is_active()) + self.assertNotEqual(domain.first_ready, None) + # Capture the value of first_ready + first_ready = domain.first_ready + # change domain status + domain.dns_needed() + self.assertFalse(domain.is_active()) + # change back to READY + domain.ready() + self.assertTrue(domain.is_active()) + # assert that the value of first_ready has not changed + self.assertEqual(domain.first_ready, first_ready) def tearDown(self) -> None: PublicContact.objects.all().delete() @@ -557,37 +560,32 @@ class TestRegistrantContacts(MockEppLib): Then the domain has a valid security contact with CISA defaults And disclose flags are set to keep the email address hidden """ - - # making a domain should make it domain - expectedSecContact = PublicContact.get_default_security() - expectedSecContact.domain = self.domain - - self.domain.dns_needed_from_unknown() - - self.assertEqual(self.mockedSendFunction.call_count, 8) - self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 4) - self.assertEqual( - PublicContact.objects.get( + with less_console_noise(): + # making a domain should make it domain + expectedSecContact = PublicContact.get_default_security() + expectedSecContact.domain = self.domain + self.domain.dns_needed_from_unknown() + self.assertEqual(self.mockedSendFunction.call_count, 8) + self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 4) + self.assertEqual( + PublicContact.objects.get( + domain=self.domain, + contact_type=PublicContact.ContactTypeChoices.SECURITY, + ).email, + expectedSecContact.email, + ) + id = PublicContact.objects.get( domain=self.domain, contact_type=PublicContact.ContactTypeChoices.SECURITY, - ).email, - expectedSecContact.email, - ) - - id = PublicContact.objects.get( - domain=self.domain, - contact_type=PublicContact.ContactTypeChoices.SECURITY, - ).registry_id - - expectedSecContact.registry_id = id - expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=False) - expectedUpdateDomain = commands.UpdateDomain( - name=self.domain.name, - add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")], - ) - - self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) - self.mockedSendFunction.assert_any_call(expectedUpdateDomain, cleaned=True) + ).registry_id + expectedSecContact.registry_id = id + expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=False) + expectedUpdateDomain = commands.UpdateDomain( + name=self.domain.name, + add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")], + ) + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + self.mockedSendFunction.assert_any_call(expectedUpdateDomain, cleaned=True) def test_user_adds_security_email(self): """ @@ -598,35 +596,31 @@ class TestRegistrantContacts(MockEppLib): And Domain sends `commands.UpdateDomain` to the registry with the newly created contact of type 'security' """ - # make a security contact that is a PublicContact - # make sure a security email already exists - self.domain.dns_needed_from_unknown() - expectedSecContact = PublicContact.get_default_security() - expectedSecContact.domain = self.domain - expectedSecContact.email = "newEmail@fake.com" - expectedSecContact.registry_id = "456" - expectedSecContact.name = "Fakey McFakerson" - - # calls the security contact setter as if you did - # self.domain.security_contact=expectedSecContact - expectedSecContact.save() - - # no longer the default email it should be disclosed - expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=True) - - expectedUpdateDomain = commands.UpdateDomain( - name=self.domain.name, - add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")], - ) - - # check that send has triggered the create command for the contact - receivedSecurityContact = PublicContact.objects.get( - domain=self.domain, contact_type=PublicContact.ContactTypeChoices.SECURITY - ) - - self.assertEqual(receivedSecurityContact, expectedSecContact) - self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) - self.mockedSendFunction.assert_any_call(expectedUpdateDomain, cleaned=True) + with less_console_noise(): + # make a security contact that is a PublicContact + # make sure a security email already exists + self.domain.dns_needed_from_unknown() + expectedSecContact = PublicContact.get_default_security() + expectedSecContact.domain = self.domain + expectedSecContact.email = "newEmail@fake.com" + expectedSecContact.registry_id = "456" + expectedSecContact.name = "Fakey McFakerson" + # calls the security contact setter as if you did + # self.domain.security_contact=expectedSecContact + expectedSecContact.save() + # no longer the default email it should be disclosed + expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=True) + expectedUpdateDomain = commands.UpdateDomain( + name=self.domain.name, + add=[common.DomainContact(contact=expectedSecContact.registry_id, type="security")], + ) + # check that send has triggered the create command for the contact + receivedSecurityContact = PublicContact.objects.get( + domain=self.domain, contact_type=PublicContact.ContactTypeChoices.SECURITY + ) + self.assertEqual(receivedSecurityContact, expectedSecContact) + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + self.mockedSendFunction.assert_any_call(expectedUpdateDomain, cleaned=True) def test_security_email_is_idempotent(self): """ @@ -635,26 +629,23 @@ class TestRegistrantContacts(MockEppLib): to the registry twice with identical data Then no errors are raised in Domain """ - - security_contact = self.domain.get_default_security_contact() - security_contact.registry_id = "fail" - security_contact.save() - - self.domain.security_contact = security_contact - - expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=False) - - expectedUpdateDomain = commands.UpdateDomain( - name=self.domain.name, - add=[common.DomainContact(contact=security_contact.registry_id, type="security")], - ) - expected_calls = [ - call(expectedCreateCommand, cleaned=True), - call(expectedCreateCommand, cleaned=True), - call(expectedUpdateDomain, cleaned=True), - ] - self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) - self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 1) + with less_console_noise(): + security_contact = self.domain.get_default_security_contact() + security_contact.registry_id = "fail" + security_contact.save() + self.domain.security_contact = security_contact + expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=False) + expectedUpdateDomain = commands.UpdateDomain( + name=self.domain.name, + add=[common.DomainContact(contact=security_contact.registry_id, type="security")], + ) + expected_calls = [ + call(expectedCreateCommand, cleaned=True), + call(expectedCreateCommand, cleaned=True), + call(expectedUpdateDomain, cleaned=True), + ] + self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) + self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 1) def test_user_deletes_security_email(self): """ @@ -667,51 +658,47 @@ class TestRegistrantContacts(MockEppLib): And the domain has a valid security contact with CISA defaults And disclose flags are set to keep the email address hidden """ - old_contact = self.domain.get_default_security_contact() - - old_contact.registry_id = "fail" - old_contact.email = "user.entered@email.com" - old_contact.save() - new_contact = self.domain.get_default_security_contact() - new_contact.registry_id = "fail" - new_contact.email = "" - self.domain.security_contact = new_contact - - firstCreateContactCall = self._convertPublicContactToEpp(old_contact, disclose_email=True) - updateDomainAddCall = commands.UpdateDomain( - name=self.domain.name, - add=[common.DomainContact(contact=old_contact.registry_id, type="security")], - ) - self.assertEqual( - PublicContact.objects.filter(domain=self.domain).get().email, - PublicContact.get_default_security().email, - ) - # this one triggers the fail - secondCreateContact = self._convertPublicContactToEpp(new_contact, disclose_email=True) - updateDomainRemCall = commands.UpdateDomain( - name=self.domain.name, - rem=[common.DomainContact(contact=old_contact.registry_id, type="security")], - ) - - defaultSecID = PublicContact.objects.filter(domain=self.domain).get().registry_id - default_security = PublicContact.get_default_security() - default_security.registry_id = defaultSecID - createDefaultContact = self._convertPublicContactToEpp(default_security, disclose_email=False) - updateDomainWDefault = commands.UpdateDomain( - name=self.domain.name, - add=[common.DomainContact(contact=defaultSecID, type="security")], - ) - - expected_calls = [ - call(firstCreateContactCall, cleaned=True), - call(updateDomainAddCall, cleaned=True), - call(secondCreateContact, cleaned=True), - call(updateDomainRemCall, cleaned=True), - call(createDefaultContact, cleaned=True), - call(updateDomainWDefault, cleaned=True), - ] - - self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) + with less_console_noise(): + old_contact = self.domain.get_default_security_contact() + old_contact.registry_id = "fail" + old_contact.email = "user.entered@email.com" + old_contact.save() + new_contact = self.domain.get_default_security_contact() + new_contact.registry_id = "fail" + new_contact.email = "" + self.domain.security_contact = new_contact + firstCreateContactCall = self._convertPublicContactToEpp(old_contact, disclose_email=True) + updateDomainAddCall = commands.UpdateDomain( + name=self.domain.name, + add=[common.DomainContact(contact=old_contact.registry_id, type="security")], + ) + self.assertEqual( + PublicContact.objects.filter(domain=self.domain).get().email, + PublicContact.get_default_security().email, + ) + # this one triggers the fail + secondCreateContact = self._convertPublicContactToEpp(new_contact, disclose_email=True) + updateDomainRemCall = commands.UpdateDomain( + name=self.domain.name, + rem=[common.DomainContact(contact=old_contact.registry_id, type="security")], + ) + defaultSecID = PublicContact.objects.filter(domain=self.domain).get().registry_id + default_security = PublicContact.get_default_security() + default_security.registry_id = defaultSecID + createDefaultContact = self._convertPublicContactToEpp(default_security, disclose_email=False) + updateDomainWDefault = commands.UpdateDomain( + name=self.domain.name, + add=[common.DomainContact(contact=defaultSecID, type="security")], + ) + expected_calls = [ + call(firstCreateContactCall, cleaned=True), + call(updateDomainAddCall, cleaned=True), + call(secondCreateContact, cleaned=True), + call(updateDomainRemCall, cleaned=True), + call(createDefaultContact, cleaned=True), + call(updateDomainWDefault, cleaned=True), + ] + self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) def test_updates_security_email(self): """ @@ -721,29 +708,28 @@ class TestRegistrantContacts(MockEppLib): security contact email Then Domain sends `commands.UpdateContact` to the registry """ - security_contact = self.domain.get_default_security_contact() - security_contact.email = "originalUserEmail@gmail.com" - security_contact.registry_id = "fail" - security_contact.save() - expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=True) - - expectedUpdateDomain = commands.UpdateDomain( - name=self.domain.name, - add=[common.DomainContact(contact=security_contact.registry_id, type="security")], - ) - security_contact.email = "changedEmail@email.com" - security_contact.save() - expectedSecondCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=True) - updateContact = self._convertPublicContactToEpp(security_contact, disclose_email=True, createContact=False) - - expected_calls = [ - call(expectedCreateCommand, cleaned=True), - call(expectedUpdateDomain, cleaned=True), - call(expectedSecondCreateCommand, cleaned=True), - call(updateContact, cleaned=True), - ] - self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) - self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 1) + with less_console_noise(): + security_contact = self.domain.get_default_security_contact() + security_contact.email = "originalUserEmail@gmail.com" + security_contact.registry_id = "fail" + security_contact.save() + expectedCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=True) + expectedUpdateDomain = commands.UpdateDomain( + name=self.domain.name, + add=[common.DomainContact(contact=security_contact.registry_id, type="security")], + ) + security_contact.email = "changedEmail@email.com" + security_contact.save() + expectedSecondCreateCommand = self._convertPublicContactToEpp(security_contact, disclose_email=True) + updateContact = self._convertPublicContactToEpp(security_contact, disclose_email=True, createContact=False) + expected_calls = [ + call(expectedCreateCommand, cleaned=True), + call(expectedUpdateDomain, cleaned=True), + call(expectedSecondCreateCommand, cleaned=True), + call(updateContact, cleaned=True), + ] + self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True) + self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 1) def test_security_email_returns_on_registry_error(self): """ @@ -751,28 +737,26 @@ class TestRegistrantContacts(MockEppLib): Registry is unavailable and throws exception when attempting to build cache from registry. Security email retrieved from database. """ - # Use self.domain_contact which has been initialized with existing contacts, including securityContact + with less_console_noise(): + # Use self.domain_contact which has been initialized with existing contacts, including securityContact + # call get_security_email to initially set the security_contact_registry_id in the domain model + self.domain_contact.get_security_email() + # invalidate the cache so the next time get_security_email is called, it has to attempt to populate cache + self.domain_contact._invalidate_cache() - # call get_security_email to initially set the security_contact_registry_id in the domain model - self.domain_contact.get_security_email() - # invalidate the cache so the next time get_security_email is called, it has to attempt to populate cache - self.domain_contact._invalidate_cache() + # mock that registry throws an error on the EPP send + def side_effect(_request, cleaned): + raise RegistryError(code=ErrorCode.COMMAND_FAILED) - # mock that registry throws an error on the EPP send - def side_effect(_request, cleaned): - raise RegistryError(code=ErrorCode.COMMAND_FAILED) - - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - # when get_security_email is called, the registry error will force the security contact - # to be retrieved using the security_contact_registry_id in the domain model - security_email = self.domain_contact.get_security_email() - - # assert that the proper security contact was retrieved by testing the email matches expected value - self.assertEqual(security_email, "security@mail.gov") - patcher.stop() + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + # when get_security_email is called, the registry error will force the security contact + # to be retrieved using the security_contact_registry_id in the domain model + security_email = self.domain_contact.get_security_email() + # assert that the proper security contact was retrieved by testing the email matches expected value + self.assertEqual(security_email, "security@mail.gov") + patcher.stop() def test_security_email_stored_on_fetch_cache(self): """ @@ -781,13 +765,14 @@ class TestRegistrantContacts(MockEppLib): The mocked data for the EPP calls for the freeman.gov domain returns a security contact with registry id of securityContact when InfoContact is called """ - # Use self.domain_contact which has been initialized with existing contacts, including securityContact + with less_console_noise(): + # Use self.domain_contact which has been initialized with existing contacts, including securityContact - # force fetch_cache to be called, which will return above documented mocked hosts - self.domain_contact.get_security_email() + # force fetch_cache to be called, which will return above documented mocked hosts + self.domain_contact.get_security_email() - # assert that the security_contact_registry_id in the db matches "securityContact" - self.assertEqual(self.domain_contact.security_contact_registry_id, "securityContact") + # assert that the security_contact_registry_id in the db matches "securityContact" + self.assertEqual(self.domain_contact.security_contact_registry_id, "securityContact") def test_not_disclosed_on_other_contacts(self): """ @@ -798,113 +783,101 @@ class TestRegistrantContacts(MockEppLib): And the field `disclose` is set to false for DF.EMAIL on all fields except security """ - # Generates a domain with four existing contacts - domain, _ = Domain.objects.get_or_create(name="freeman.gov") - - # Contact setup - expected_admin = domain.get_default_administrative_contact() - expected_admin.email = self.mockAdministrativeContact.email - - expected_registrant = domain.get_default_registrant_contact() - expected_registrant.email = self.mockRegistrantContact.email - - expected_security = domain.get_default_security_contact() - expected_security.email = self.mockSecurityContact.email - - expected_tech = domain.get_default_technical_contact() - expected_tech.email = self.mockTechnicalContact.email - - domain.administrative_contact = expected_admin - domain.registrant_contact = expected_registrant - domain.security_contact = expected_security - domain.technical_contact = expected_tech - - contacts = [ - (expected_admin, domain.administrative_contact), - (expected_registrant, domain.registrant_contact), - (expected_security, domain.security_contact), - (expected_tech, domain.technical_contact), - ] - - # Test for each contact - for contact in contacts: - expected_contact = contact[0] - actual_contact = contact[1] - is_security = expected_contact.contact_type == "security" - - expectedCreateCommand = self._convertPublicContactToEpp(expected_contact, disclose_email=is_security) - - # Should only be disclosed if the type is security, as the email is valid - self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) - - # The emails should match on both items - self.assertEqual(expected_contact.email, actual_contact.email) + with less_console_noise(): + # Generates a domain with four existing contacts + domain, _ = Domain.objects.get_or_create(name="freeman.gov") + # Contact setup + expected_admin = domain.get_default_administrative_contact() + expected_admin.email = self.mockAdministrativeContact.email + expected_registrant = domain.get_default_registrant_contact() + expected_registrant.email = self.mockRegistrantContact.email + expected_security = domain.get_default_security_contact() + expected_security.email = self.mockSecurityContact.email + expected_tech = domain.get_default_technical_contact() + expected_tech.email = self.mockTechnicalContact.email + domain.administrative_contact = expected_admin + domain.registrant_contact = expected_registrant + domain.security_contact = expected_security + domain.technical_contact = expected_tech + contacts = [ + (expected_admin, domain.administrative_contact), + (expected_registrant, domain.registrant_contact), + (expected_security, domain.security_contact), + (expected_tech, domain.technical_contact), + ] + # Test for each contact + for contact in contacts: + expected_contact = contact[0] + actual_contact = contact[1] + is_security = expected_contact.contact_type == "security" + expectedCreateCommand = self._convertPublicContactToEpp(expected_contact, disclose_email=is_security) + # Should only be disclosed if the type is security, as the email is valid + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # The emails should match on both items + self.assertEqual(expected_contact.email, actual_contact.email) def test_convert_public_contact_to_epp(self): - domain, _ = Domain.objects.get_or_create(name="freeman.gov") - dummy_contact = domain.get_default_security_contact() - test_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=True).__dict__ - test_not_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=False).__dict__ - - # Separated for linter - disclose_email_field = {common.DiscloseField.EMAIL} - expected_disclose = { - "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), - "disclose": common.Disclose(flag=True, fields=disclose_email_field, types=None), - "email": "dotgov@cisa.dhs.gov", - "extensions": [], - "fax": None, - "id": "ThIq2NcRIDN7PauO", - "ident": None, - "notify_email": None, - "postal_info": common.PostalInfo( - name="Registry Customer Service", - addr=common.ContactAddr( - street=["4200 Wilson Blvd.", None, None], - city="Arlington", - pc="22201", - cc="US", - sp="VA", + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="freeman.gov") + dummy_contact = domain.get_default_security_contact() + test_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=True).__dict__ + test_not_disclose = self._convertPublicContactToEpp(dummy_contact, disclose_email=False).__dict__ + # Separated for linter + disclose_email_field = {common.DiscloseField.EMAIL} + expected_disclose = { + "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), + "disclose": common.Disclose(flag=True, fields=disclose_email_field, types=None), + "email": "dotgov@cisa.dhs.gov", + "extensions": [], + "fax": None, + "id": "ThIq2NcRIDN7PauO", + "ident": None, + "notify_email": None, + "postal_info": common.PostalInfo( + name="Registry Customer Service", + addr=common.ContactAddr( + street=["4200 Wilson Blvd.", None, None], + city="Arlington", + pc="22201", + cc="US", + sp="VA", + ), + org="Cybersecurity and Infrastructure Security Agency", + type="loc", ), - org="Cybersecurity and Infrastructure Security Agency", - type="loc", - ), - "vat": None, - "voice": "+1.8882820870", - } - - # Separated for linter - expected_not_disclose = { - "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), - "disclose": common.Disclose(flag=False, fields=disclose_email_field, types=None), - "email": "dotgov@cisa.dhs.gov", - "extensions": [], - "fax": None, - "id": "ThrECENCHI76PGLh", - "ident": None, - "notify_email": None, - "postal_info": common.PostalInfo( - name="Registry Customer Service", - addr=common.ContactAddr( - street=["4200 Wilson Blvd.", None, None], - city="Arlington", - pc="22201", - cc="US", - sp="VA", + "vat": None, + "voice": "+1.8882820870", + } + # Separated for linter + expected_not_disclose = { + "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"), + "disclose": common.Disclose(flag=False, fields=disclose_email_field, types=None), + "email": "dotgov@cisa.dhs.gov", + "extensions": [], + "fax": None, + "id": "ThrECENCHI76PGLh", + "ident": None, + "notify_email": None, + "postal_info": common.PostalInfo( + name="Registry Customer Service", + addr=common.ContactAddr( + street=["4200 Wilson Blvd.", None, None], + city="Arlington", + pc="22201", + cc="US", + sp="VA", + ), + org="Cybersecurity and Infrastructure Security Agency", + type="loc", ), - org="Cybersecurity and Infrastructure Security Agency", - type="loc", - ), - "vat": None, - "voice": "+1.8882820870", - } - - # Set the ids equal, since this value changes - test_disclose["id"] = expected_disclose["id"] - test_not_disclose["id"] = expected_not_disclose["id"] - - self.assertEqual(test_disclose, expected_disclose) - self.assertEqual(test_not_disclose, expected_not_disclose) + "vat": None, + "voice": "+1.8882820870", + } + # Set the ids equal, since this value changes + test_disclose["id"] = expected_disclose["id"] + test_not_disclose["id"] = expected_not_disclose["id"] + self.assertEqual(test_disclose, expected_disclose) + self.assertEqual(test_not_disclose, expected_not_disclose) def test_not_disclosed_on_default_security_contact(self): """ @@ -913,17 +886,16 @@ class TestRegistrantContacts(MockEppLib): Then Domain sends `commands.CreateContact` to the registry And the field `disclose` is set to false for DF.EMAIL """ - domain, _ = Domain.objects.get_or_create(name="defaultsecurity.gov") - expectedSecContact = PublicContact.get_default_security() - expectedSecContact.domain = domain - expectedSecContact.registry_id = "defaultSec" - domain.security_contact = expectedSecContact - - expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=False) - - self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) - # Confirm that we are getting a default email - self.assertEqual(domain.security_contact.email, expectedSecContact.email) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="defaultsecurity.gov") + expectedSecContact = PublicContact.get_default_security() + expectedSecContact.domain = domain + expectedSecContact.registry_id = "defaultSec" + domain.security_contact = expectedSecContact + expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=False) + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # Confirm that we are getting a default email + self.assertEqual(domain.security_contact.email, expectedSecContact.email) def test_not_disclosed_on_default_technical_contact(self): """ @@ -932,17 +904,16 @@ class TestRegistrantContacts(MockEppLib): Then Domain sends `commands.CreateContact` to the registry And the field `disclose` is set to false for DF.EMAIL """ - domain, _ = Domain.objects.get_or_create(name="defaulttechnical.gov") - expectedTechContact = PublicContact.get_default_technical() - expectedTechContact.domain = domain - expectedTechContact.registry_id = "defaultTech" - domain.technical_contact = expectedTechContact - - expectedCreateCommand = self._convertPublicContactToEpp(expectedTechContact, disclose_email=False) - - self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) - # Confirm that we are getting a default email - self.assertEqual(domain.technical_contact.email, expectedTechContact.email) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="defaulttechnical.gov") + expectedTechContact = PublicContact.get_default_technical() + expectedTechContact.domain = domain + expectedTechContact.registry_id = "defaultTech" + domain.technical_contact = expectedTechContact + expectedCreateCommand = self._convertPublicContactToEpp(expectedTechContact, disclose_email=False) + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # Confirm that we are getting a default email + self.assertEqual(domain.technical_contact.email, expectedTechContact.email) def test_is_disclosed_on_security_contact(self): """ @@ -952,17 +923,16 @@ class TestRegistrantContacts(MockEppLib): Then Domain sends `commands.CreateContact` to the registry And the field `disclose` is set to true for DF.EMAIL """ - domain, _ = Domain.objects.get_or_create(name="igorville.gov") - expectedSecContact = PublicContact.get_default_security() - expectedSecContact.domain = domain - expectedSecContact.email = "123@mail.gov" - domain.security_contact = expectedSecContact - - expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=True) - - self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) - # Confirm that we are getting the desired email - self.assertEqual(domain.security_contact.email, expectedSecContact.email) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="igorville.gov") + expectedSecContact = PublicContact.get_default_security() + expectedSecContact.domain = domain + expectedSecContact.email = "123@mail.gov" + domain.security_contact = expectedSecContact + expectedCreateCommand = self._convertPublicContactToEpp(expectedSecContact, disclose_email=True) + self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True) + # Confirm that we are getting the desired email + self.assertEqual(domain.security_contact.email, expectedSecContact.email) @skip("not implemented yet") def test_update_is_unsuccessful(self): @@ -974,121 +944,112 @@ class TestRegistrantContacts(MockEppLib): raise def test_contact_getter_security(self): - security = PublicContact.ContactTypeChoices.SECURITY - # Create prexisting object - expected_contact = self.domain.map_epp_contact_to_public_contact( - self.mockSecurityContact, - contact_id="securityContact", - contact_type=security, - ) - - # Checks if we grabbed the correct PublicContact - self.assertEqual(self.domain_contact.security_contact.email, expected_contact.email) - - expected_contact_db = PublicContact.objects.filter( - registry_id=self.domain_contact.security_contact.registry_id, - contact_type=security, - ).get() - - self.assertEqual(self.domain_contact.security_contact, expected_contact_db) - - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.InfoContact(id="securityContact", auth_info=None), - cleaned=True, - ), - ] - ) - # Checks if we are receiving the cache we expect - cache = self.domain_contact._cache["contacts"] - self.assertEqual(cache.get(security), "securityContact") + with less_console_noise(): + security = PublicContact.ContactTypeChoices.SECURITY + # Create prexisting object + expected_contact = self.domain.map_epp_contact_to_public_contact( + self.mockSecurityContact, + contact_id="securityContact", + contact_type=security, + ) + # Checks if we grabbed the correct PublicContact + self.assertEqual(self.domain_contact.security_contact.email, expected_contact.email) + expected_contact_db = PublicContact.objects.filter( + registry_id=self.domain_contact.security_contact.registry_id, + contact_type=security, + ).get() + self.assertEqual(self.domain_contact.security_contact, expected_contact_db) + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.InfoContact(id="securityContact", auth_info=None), + cleaned=True, + ), + ] + ) + # Checks if we are receiving the cache we expect + cache = self.domain_contact._cache["contacts"] + self.assertEqual(cache.get(security), "securityContact") def test_contact_getter_technical(self): - technical = PublicContact.ContactTypeChoices.TECHNICAL - expected_contact = self.domain.map_epp_contact_to_public_contact( - self.mockTechnicalContact, - contact_id="technicalContact", - contact_type=technical, - ) - - self.assertEqual(self.domain_contact.technical_contact.email, expected_contact.email) - - # Checks if we grab the correct PublicContact - expected_contact_db = PublicContact.objects.filter( - registry_id=self.domain_contact.technical_contact.registry_id, - contact_type=technical, - ).get() - - # Checks if we grab the correct PublicContact - self.assertEqual(self.domain_contact.technical_contact, expected_contact_db) - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.InfoContact(id="technicalContact", auth_info=None), - cleaned=True, - ), - ] - ) - # Checks if we are receiving the cache we expect - cache = self.domain_contact._cache["contacts"] - self.assertEqual(cache.get(technical), "technicalContact") + with less_console_noise(): + technical = PublicContact.ContactTypeChoices.TECHNICAL + expected_contact = self.domain.map_epp_contact_to_public_contact( + self.mockTechnicalContact, + contact_id="technicalContact", + contact_type=technical, + ) + self.assertEqual(self.domain_contact.technical_contact.email, expected_contact.email) + # Checks if we grab the correct PublicContact + expected_contact_db = PublicContact.objects.filter( + registry_id=self.domain_contact.technical_contact.registry_id, + contact_type=technical, + ).get() + # Checks if we grab the correct PublicContact + self.assertEqual(self.domain_contact.technical_contact, expected_contact_db) + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.InfoContact(id="technicalContact", auth_info=None), + cleaned=True, + ), + ] + ) + # Checks if we are receiving the cache we expect + cache = self.domain_contact._cache["contacts"] + self.assertEqual(cache.get(technical), "technicalContact") def test_contact_getter_administrative(self): - administrative = PublicContact.ContactTypeChoices.ADMINISTRATIVE - expected_contact = self.domain.map_epp_contact_to_public_contact( - self.mockAdministrativeContact, - contact_id="adminContact", - contact_type=administrative, - ) - - self.assertEqual(self.domain_contact.administrative_contact.email, expected_contact.email) - - expected_contact_db = PublicContact.objects.filter( - registry_id=self.domain_contact.administrative_contact.registry_id, - contact_type=administrative, - ).get() - - # Checks if we grab the correct PublicContact - self.assertEqual(self.domain_contact.administrative_contact, expected_contact_db) - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.InfoContact(id="adminContact", auth_info=None), - cleaned=True, - ), - ] - ) - # Checks if we are receiving the cache we expect - cache = self.domain_contact._cache["contacts"] - self.assertEqual(cache.get(administrative), "adminContact") + with less_console_noise(): + administrative = PublicContact.ContactTypeChoices.ADMINISTRATIVE + expected_contact = self.domain.map_epp_contact_to_public_contact( + self.mockAdministrativeContact, + contact_id="adminContact", + contact_type=administrative, + ) + self.assertEqual(self.domain_contact.administrative_contact.email, expected_contact.email) + expected_contact_db = PublicContact.objects.filter( + registry_id=self.domain_contact.administrative_contact.registry_id, + contact_type=administrative, + ).get() + # Checks if we grab the correct PublicContact + self.assertEqual(self.domain_contact.administrative_contact, expected_contact_db) + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.InfoContact(id="adminContact", auth_info=None), + cleaned=True, + ), + ] + ) + # Checks if we are receiving the cache we expect + cache = self.domain_contact._cache["contacts"] + self.assertEqual(cache.get(administrative), "adminContact") def test_contact_getter_registrant(self): - expected_contact = self.domain.map_epp_contact_to_public_contact( - self.mockRegistrantContact, - contact_id="regContact", - contact_type=PublicContact.ContactTypeChoices.REGISTRANT, - ) - - self.assertEqual(self.domain_contact.registrant_contact.email, expected_contact.email) - - expected_contact_db = PublicContact.objects.filter( - registry_id=self.domain_contact.registrant_contact.registry_id, - contact_type=PublicContact.ContactTypeChoices.REGISTRANT, - ).get() - - # Checks if we grab the correct PublicContact - self.assertEqual(self.domain_contact.registrant_contact, expected_contact_db) - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.InfoContact(id="regContact", auth_info=None), - cleaned=True, - ), - ] - ) - # Checks if we are receiving the cache we expect. - self.assertEqual(self.domain_contact._cache["registrant"], expected_contact_db) + with less_console_noise(): + expected_contact = self.domain.map_epp_contact_to_public_contact( + self.mockRegistrantContact, + contact_id="regContact", + contact_type=PublicContact.ContactTypeChoices.REGISTRANT, + ) + self.assertEqual(self.domain_contact.registrant_contact.email, expected_contact.email) + expected_contact_db = PublicContact.objects.filter( + registry_id=self.domain_contact.registrant_contact.registry_id, + contact_type=PublicContact.ContactTypeChoices.REGISTRANT, + ).get() + # Checks if we grab the correct PublicContact + self.assertEqual(self.domain_contact.registrant_contact, expected_contact_db) + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.InfoContact(id="regContact", auth_info=None), + cleaned=True, + ), + ] + ) + # Checks if we are receiving the cache we expect. + self.assertEqual(self.domain_contact._cache["registrant"], expected_contact_db) class TestRegistrantNameservers(MockEppLib): @@ -1112,76 +1073,78 @@ class TestRegistrantNameservers(MockEppLib): def test_get_nameserver_changes_success_deleted_vals(self): """Testing only deleting and no other changes""" - self.domain._cache["hosts"] = [ - {"name": "ns1.example.com", "addrs": None}, - {"name": "ns2.example.com", "addrs": ["1.2.3.4"]}, - ] - newChanges = [ - ("ns1.example.com",), - ] - ( - deleted_values, - updated_values, - new_values, - oldNameservers, - ) = self.domain.getNameserverChanges(newChanges) + with less_console_noise(): + self.domain._cache["hosts"] = [ + {"name": "ns1.example.com", "addrs": None}, + {"name": "ns2.example.com", "addrs": ["1.2.3.4"]}, + ] + newChanges = [ + ("ns1.example.com",), + ] + ( + deleted_values, + updated_values, + new_values, + oldNameservers, + ) = self.domain.getNameserverChanges(newChanges) - self.assertEqual(deleted_values, ["ns2.example.com"]) - self.assertEqual(updated_values, []) - self.assertEqual(new_values, {}) - self.assertEqual( - oldNameservers, - {"ns1.example.com": None, "ns2.example.com": ["1.2.3.4"]}, - ) + self.assertEqual(deleted_values, ["ns2.example.com"]) + self.assertEqual(updated_values, []) + self.assertEqual(new_values, {}) + self.assertEqual( + oldNameservers, + {"ns1.example.com": None, "ns2.example.com": ["1.2.3.4"]}, + ) def test_get_nameserver_changes_success_updated_vals(self): """Testing only updating no other changes""" - self.domain._cache["hosts"] = [ - {"name": "ns3.my-nameserver.gov", "addrs": ["1.2.3.4"]}, - ] - newChanges = [ - ("ns3.my-nameserver.gov", ["1.2.4.5"]), - ] - ( - deleted_values, - updated_values, - new_values, - oldNameservers, - ) = self.domain.getNameserverChanges(newChanges) - - self.assertEqual(deleted_values, []) - self.assertEqual(updated_values, [("ns3.my-nameserver.gov", ["1.2.4.5"])]) - self.assertEqual(new_values, {}) - self.assertEqual( - oldNameservers, - {"ns3.my-nameserver.gov": ["1.2.3.4"]}, - ) + with less_console_noise(): + self.domain._cache["hosts"] = [ + {"name": "ns3.my-nameserver.gov", "addrs": ["1.2.3.4"]}, + ] + newChanges = [ + ("ns3.my-nameserver.gov", ["1.2.4.5"]), + ] + ( + deleted_values, + updated_values, + new_values, + oldNameservers, + ) = self.domain.getNameserverChanges(newChanges) + self.assertEqual(deleted_values, []) + self.assertEqual(updated_values, [("ns3.my-nameserver.gov", ["1.2.4.5"])]) + self.assertEqual(new_values, {}) + self.assertEqual( + oldNameservers, + {"ns3.my-nameserver.gov": ["1.2.3.4"]}, + ) def test_get_nameserver_changes_success_new_vals(self): - # Testing only creating no other changes - self.domain._cache["hosts"] = [ - {"name": "ns1.example.com", "addrs": None}, - ] - newChanges = [ - ("ns1.example.com",), - ("ns4.example.com",), - ] - ( - deleted_values, - updated_values, - new_values, - oldNameservers, - ) = self.domain.getNameserverChanges(newChanges) + with less_console_noise(): + # Testing only creating no other changes + self.domain._cache["hosts"] = [ + {"name": "ns1.example.com", "addrs": None}, + ] + newChanges = [ + ("ns1.example.com",), + ("ns4.example.com",), + ] + ( + deleted_values, + updated_values, + new_values, + oldNameservers, + ) = self.domain.getNameserverChanges(newChanges) - self.assertEqual(deleted_values, []) - self.assertEqual(updated_values, []) - self.assertEqual(new_values, {"ns4.example.com": None}) - self.assertEqual( - oldNameservers, - { - "ns1.example.com": None, - }, - ) + self.assertEqual(deleted_values, []) + self.assertEqual(updated_values, []) + self.assertEqual(new_values, {"ns4.example.com": None}) + self.assertEqual( + oldNameservers, + { + "ns1.example.com": None, + }, + ) def test_user_adds_one_nameserver(self): """ @@ -1193,32 +1156,27 @@ class TestRegistrantNameservers(MockEppLib): And `domain.is_active` returns False And domain.first_ready is null """ - - # set 1 nameserver - nameserver = "ns1.my-nameserver.com" - self.domain.nameservers = [(nameserver,)] - - # when we create a host, we should've updated at the same time - created_host = commands.CreateHost(nameserver) - update_domain_with_created = commands.UpdateDomain( - name=self.domain.name, - add=[common.HostObjSet([created_host.name])], - rem=[], - ) - - # checking if commands were sent (commands have to be sent in order) - expectedCalls = [ - call(created_host, cleaned=True), - call(update_domain_with_created, cleaned=True), - ] - - self.mockedSendFunction.assert_has_calls(expectedCalls) - - # check that status is still NOT READY - # as you have less than 2 nameservers - self.assertFalse(self.domain.is_active()) - - self.assertEqual(self.domain.first_ready, None) + with less_console_noise(): + # set 1 nameserver + nameserver = "ns1.my-nameserver.com" + self.domain.nameservers = [(nameserver,)] + # when we create a host, we should've updated at the same time + created_host = commands.CreateHost(nameserver) + update_domain_with_created = commands.UpdateDomain( + name=self.domain.name, + add=[common.HostObjSet([created_host.name])], + rem=[], + ) + # checking if commands were sent (commands have to be sent in order) + expectedCalls = [ + call(created_host, cleaned=True), + call(update_domain_with_created, cleaned=True), + ] + self.mockedSendFunction.assert_has_calls(expectedCalls) + # check that status is still NOT READY + # as you have less than 2 nameservers + self.assertFalse(self.domain.is_active()) + self.assertEqual(self.domain.first_ready, None) def test_user_adds_two_nameservers(self): """ @@ -1230,36 +1188,32 @@ class TestRegistrantNameservers(MockEppLib): And `domain.is_active` returns True And domain.first_ready is not null """ - - # set 2 nameservers - self.domain.nameservers = [(self.nameserver1,), (self.nameserver2,)] - - # when you create a host, you also have to update at same time - created_host1 = commands.CreateHost(self.nameserver1) - created_host2 = commands.CreateHost(self.nameserver2) - - update_domain_with_created = commands.UpdateDomain( - name=self.domain.name, - add=[ - common.HostObjSet([created_host1.name, created_host2.name]), - ], - rem=[], - ) - - infoDomain = commands.InfoDomain(name="my-nameserver.gov", auth_info=None) - # checking if commands were sent (commands have to be sent in order) - expectedCalls = [ - call(infoDomain, cleaned=True), - call(created_host1, cleaned=True), - call(created_host2, cleaned=True), - call(update_domain_with_created, cleaned=True), - ] - - self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) - self.assertEqual(4, self.mockedSendFunction.call_count) - # check that status is READY - self.assertTrue(self.domain.is_active()) - self.assertNotEqual(self.domain.first_ready, None) + with less_console_noise(): + # set 2 nameservers + self.domain.nameservers = [(self.nameserver1,), (self.nameserver2,)] + # when you create a host, you also have to update at same time + created_host1 = commands.CreateHost(self.nameserver1) + created_host2 = commands.CreateHost(self.nameserver2) + update_domain_with_created = commands.UpdateDomain( + name=self.domain.name, + add=[ + common.HostObjSet([created_host1.name, created_host2.name]), + ], + rem=[], + ) + infoDomain = commands.InfoDomain(name="my-nameserver.gov", auth_info=None) + # checking if commands were sent (commands have to be sent in order) + expectedCalls = [ + call(infoDomain, cleaned=True), + call(created_host1, cleaned=True), + call(created_host2, cleaned=True), + call(update_domain_with_created, cleaned=True), + ] + self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) + self.assertEqual(4, self.mockedSendFunction.call_count) + # check that status is READY + self.assertTrue(self.domain.is_active()) + self.assertNotEqual(self.domain.first_ready, None) def test_user_adds_too_many_nameservers(self): """ @@ -1268,43 +1222,43 @@ class TestRegistrantNameservers(MockEppLib): When `domain.nameservers` is set to an array of length 14 Then Domain raises a user-friendly error """ + with less_console_noise(): + # set 13+ nameservers + nameserver1 = "ns1.cats-are-superior1.com" + nameserver2 = "ns1.cats-are-superior2.com" + nameserver3 = "ns1.cats-are-superior3.com" + nameserver4 = "ns1.cats-are-superior4.com" + nameserver5 = "ns1.cats-are-superior5.com" + nameserver6 = "ns1.cats-are-superior6.com" + nameserver7 = "ns1.cats-are-superior7.com" + nameserver8 = "ns1.cats-are-superior8.com" + nameserver9 = "ns1.cats-are-superior9.com" + nameserver10 = "ns1.cats-are-superior10.com" + nameserver11 = "ns1.cats-are-superior11.com" + nameserver12 = "ns1.cats-are-superior12.com" + nameserver13 = "ns1.cats-are-superior13.com" + nameserver14 = "ns1.cats-are-superior14.com" - # set 13+ nameservers - nameserver1 = "ns1.cats-are-superior1.com" - nameserver2 = "ns1.cats-are-superior2.com" - nameserver3 = "ns1.cats-are-superior3.com" - nameserver4 = "ns1.cats-are-superior4.com" - nameserver5 = "ns1.cats-are-superior5.com" - nameserver6 = "ns1.cats-are-superior6.com" - nameserver7 = "ns1.cats-are-superior7.com" - nameserver8 = "ns1.cats-are-superior8.com" - nameserver9 = "ns1.cats-are-superior9.com" - nameserver10 = "ns1.cats-are-superior10.com" - nameserver11 = "ns1.cats-are-superior11.com" - nameserver12 = "ns1.cats-are-superior12.com" - nameserver13 = "ns1.cats-are-superior13.com" - nameserver14 = "ns1.cats-are-superior14.com" + def _get_14_nameservers(): + self.domain.nameservers = [ + (nameserver1,), + (nameserver2,), + (nameserver3,), + (nameserver4,), + (nameserver5,), + (nameserver6,), + (nameserver7,), + (nameserver8,), + (nameserver9), + (nameserver10,), + (nameserver11,), + (nameserver12,), + (nameserver13,), + (nameserver14,), + ] - def _get_14_nameservers(): - self.domain.nameservers = [ - (nameserver1,), - (nameserver2,), - (nameserver3,), - (nameserver4,), - (nameserver5,), - (nameserver6,), - (nameserver7,), - (nameserver8,), - (nameserver9), - (nameserver10,), - (nameserver11,), - (nameserver12,), - (nameserver13,), - (nameserver14,), - ] - - self.assertRaises(NameserverError, _get_14_nameservers) - self.assertEqual(self.mockedSendFunction.call_count, 0) + self.assertRaises(NameserverError, _get_14_nameservers) + self.assertEqual(self.mockedSendFunction.call_count, 0) def test_user_removes_some_nameservers(self): """ @@ -1315,37 +1269,36 @@ class TestRegistrantNameservers(MockEppLib): to the registry And `domain.is_active` returns True """ - - # Mock is set to return 3 nameservers on infodomain - self.domainWithThreeNS.nameservers = [(self.nameserver1,), (self.nameserver2,)] - expectedCalls = [ - # calls info domain, and info on all hosts - # to get past values - # then removes the single host and updates domain - call( - commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), - cleaned=True, - ), - call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True), - call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True), - call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True), - call( - commands.UpdateDomain( - name=self.domainWithThreeNS.name, - add=[], - rem=[common.HostObjSet(hosts=["ns1.cats-are-superior3.com"])], - nsset=None, - keyset=None, - registrant=None, - auth_info=None, + with less_console_noise(): + # Mock is set to return 3 nameservers on infodomain + self.domainWithThreeNS.nameservers = [(self.nameserver1,), (self.nameserver2,)] + expectedCalls = [ + # calls info domain, and info on all hosts + # to get past values + # then removes the single host and updates domain + call( + commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), + cleaned=True, ), - cleaned=True, - ), - call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True), - ] - - self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) - self.assertTrue(self.domainWithThreeNS.is_active()) + call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True), + call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True), + call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True), + call( + commands.UpdateDomain( + name=self.domainWithThreeNS.name, + add=[], + rem=[common.HostObjSet(hosts=["ns1.cats-are-superior3.com"])], + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ), + call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True), + ] + self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) + self.assertTrue(self.domainWithThreeNS.is_active()) def test_user_removes_too_many_nameservers(self): """ @@ -1357,41 +1310,40 @@ class TestRegistrantNameservers(MockEppLib): And `domain.is_active` returns False """ - - self.domainWithThreeNS.nameservers = [(self.nameserver1,)] - expectedCalls = [ - call( - commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), - cleaned=True, - ), - call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True), - call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True), - call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True), - call(commands.DeleteHost(name="ns1.my-nameserver-2.com"), cleaned=True), - call( - commands.UpdateDomain( - name=self.domainWithThreeNS.name, - add=[], - rem=[ - common.HostObjSet( - hosts=[ - "ns1.my-nameserver-2.com", - "ns1.cats-are-superior3.com", - ] - ), - ], - nsset=None, - keyset=None, - registrant=None, - auth_info=None, + with less_console_noise(): + self.domainWithThreeNS.nameservers = [(self.nameserver1,)] + expectedCalls = [ + call( + commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), + cleaned=True, ), - cleaned=True, - ), - call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True), - ] - - self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) - self.assertFalse(self.domainWithThreeNS.is_active()) + call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True), + call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True), + call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True), + call(commands.DeleteHost(name="ns1.my-nameserver-2.com"), cleaned=True), + call( + commands.UpdateDomain( + name=self.domainWithThreeNS.name, + add=[], + rem=[ + common.HostObjSet( + hosts=[ + "ns1.my-nameserver-2.com", + "ns1.cats-are-superior3.com", + ] + ), + ], + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ), + call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True), + ] + self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) + self.assertFalse(self.domainWithThreeNS.is_active()) def test_user_replaces_nameservers(self): """ @@ -1403,59 +1355,58 @@ class TestRegistrantNameservers(MockEppLib): And `commands.UpdateDomain` is sent to add #4 and #5 plus remove #2 and #3 And `commands.DeleteHost` is sent to delete #2 and #3 """ - self.domainWithThreeNS.nameservers = [ - (self.nameserver1,), - ("ns1.cats-are-superior1.com",), - ("ns1.cats-are-superior2.com",), - ] - - expectedCalls = [ - call( - commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), - cleaned=True, - ), - call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True), - call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True), - call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True), - call(commands.DeleteHost(name="ns1.my-nameserver-2.com"), cleaned=True), - call( - commands.CreateHost(name="ns1.cats-are-superior1.com", addrs=[]), - cleaned=True, - ), - call( - commands.CreateHost(name="ns1.cats-are-superior2.com", addrs=[]), - cleaned=True, - ), - call( - commands.UpdateDomain( - name=self.domainWithThreeNS.name, - add=[ - common.HostObjSet( - hosts=[ - "ns1.cats-are-superior1.com", - "ns1.cats-are-superior2.com", - ] - ), - ], - rem=[ - common.HostObjSet( - hosts=[ - "ns1.my-nameserver-2.com", - "ns1.cats-are-superior3.com", - ] - ), - ], - nsset=None, - keyset=None, - registrant=None, - auth_info=None, + with less_console_noise(): + self.domainWithThreeNS.nameservers = [ + (self.nameserver1,), + ("ns1.cats-are-superior1.com",), + ("ns1.cats-are-superior2.com",), + ] + expectedCalls = [ + call( + commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), + cleaned=True, ), - cleaned=True, - ), - ] - - self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) - self.assertTrue(self.domainWithThreeNS.is_active()) + call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True), + call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True), + call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True), + call(commands.DeleteHost(name="ns1.my-nameserver-2.com"), cleaned=True), + call( + commands.CreateHost(name="ns1.cats-are-superior1.com", addrs=[]), + cleaned=True, + ), + call( + commands.CreateHost(name="ns1.cats-are-superior2.com", addrs=[]), + cleaned=True, + ), + call( + commands.UpdateDomain( + name=self.domainWithThreeNS.name, + add=[ + common.HostObjSet( + hosts=[ + "ns1.cats-are-superior1.com", + "ns1.cats-are-superior2.com", + ] + ), + ], + rem=[ + common.HostObjSet( + hosts=[ + "ns1.my-nameserver-2.com", + "ns1.cats-are-superior3.com", + ] + ), + ], + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ), + ] + self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) + self.assertTrue(self.domainWithThreeNS.is_active()) def test_user_cannot_add_subordinate_without_ip(self): """ @@ -1465,11 +1416,10 @@ class TestRegistrantNameservers(MockEppLib): with a subdomain of the domain and no IP addresses Then Domain raises a user-friendly error """ - - dotgovnameserver = "my-nameserver.gov" - - with self.assertRaises(NameserverError): - self.domain.nameservers = [(dotgovnameserver,)] + with less_console_noise(): + dotgovnameserver = "my-nameserver.gov" + with self.assertRaises(NameserverError): + self.domain.nameservers = [(dotgovnameserver,)] def test_user_updates_ips(self): """ @@ -1480,46 +1430,45 @@ class TestRegistrantNameservers(MockEppLib): with a different IP address(es) Then `commands.UpdateHost` is sent to the registry """ - domain, _ = Domain.objects.get_or_create(name="nameserverwithip.gov", state=Domain.State.READY) - domain.nameservers = [ - ("ns1.nameserverwithip.gov", ["2.3.4.5", "1.2.3.4"]), - ( - "ns2.nameserverwithip.gov", - ["1.2.3.4", "2.3.4.5", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"], - ), - ("ns3.nameserverwithip.gov", ["2.3.4.5"]), - ] - - expectedCalls = [ - call( - commands.InfoDomain(name="nameserverwithip.gov", auth_info=None), - cleaned=True, - ), - call(commands.InfoHost(name="ns1.nameserverwithip.gov"), cleaned=True), - call(commands.InfoHost(name="ns2.nameserverwithip.gov"), cleaned=True), - call(commands.InfoHost(name="ns3.nameserverwithip.gov"), cleaned=True), - call( - commands.UpdateHost( - name="ns2.nameserverwithip.gov", - add=[common.Ip(addr="2001:0db8:85a3:0000:0000:8a2e:0370:7334", ip="v6")], - rem=[], - chg=None, + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="nameserverwithip.gov", state=Domain.State.READY) + domain.nameservers = [ + ("ns1.nameserverwithip.gov", ["2.3.4.5", "1.2.3.4"]), + ( + "ns2.nameserverwithip.gov", + ["1.2.3.4", "2.3.4.5", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"], ), - cleaned=True, - ), - call( - commands.UpdateHost( - name="ns3.nameserverwithip.gov", - add=[], - rem=[common.Ip(addr="1.2.3.4", ip=None)], - chg=None, + ("ns3.nameserverwithip.gov", ["2.3.4.5"]), + ] + expectedCalls = [ + call( + commands.InfoDomain(name="nameserverwithip.gov", auth_info=None), + cleaned=True, ), - cleaned=True, - ), - ] - - self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) - self.assertTrue(domain.is_active()) + call(commands.InfoHost(name="ns1.nameserverwithip.gov"), cleaned=True), + call(commands.InfoHost(name="ns2.nameserverwithip.gov"), cleaned=True), + call(commands.InfoHost(name="ns3.nameserverwithip.gov"), cleaned=True), + call( + commands.UpdateHost( + name="ns2.nameserverwithip.gov", + add=[common.Ip(addr="2001:0db8:85a3:0000:0000:8a2e:0370:7334", ip="v6")], + rem=[], + chg=None, + ), + cleaned=True, + ), + call( + commands.UpdateHost( + name="ns3.nameserverwithip.gov", + add=[], + rem=[common.Ip(addr="1.2.3.4", ip=None)], + chg=None, + ), + cleaned=True, + ), + ] + self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) + self.assertTrue(domain.is_active()) def test_user_cannot_add_non_subordinate_with_ip(self): """ @@ -1529,10 +1478,10 @@ class TestRegistrantNameservers(MockEppLib): which is not a subdomain of the domain and has IP addresses Then Domain raises a user-friendly error """ - dotgovnameserver = "mynameserverdotgov.gov" - - with self.assertRaises(NameserverError): - self.domain.nameservers = [(dotgovnameserver, ["1.2.3"])] + with less_console_noise(): + dotgovnameserver = "mynameserverdotgov.gov" + with self.assertRaises(NameserverError): + self.domain.nameservers = [(dotgovnameserver, ["1.2.3"])] def test_nameservers_are_idempotent(self): """ @@ -1541,60 +1490,60 @@ class TestRegistrantNameservers(MockEppLib): to the registry twice with identical data Then no errors are raised in Domain """ - - # Checking that it doesn't create or update even if out of order - self.domainWithThreeNS.nameservers = [ - (self.nameserver3,), - (self.nameserver1,), - (self.nameserver2,), - ] - - expectedCalls = [ - call( - commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), - cleaned=True, - ), - call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True), - call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True), - call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True), - ] - - self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) - self.assertEqual(self.mockedSendFunction.call_count, 4) + with less_console_noise(): + # Checking that it doesn't create or update even if out of order + self.domainWithThreeNS.nameservers = [ + (self.nameserver3,), + (self.nameserver1,), + (self.nameserver2,), + ] + expectedCalls = [ + call( + commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None), + cleaned=True, + ), + call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True), + call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True), + call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True), + ] + self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True) + self.assertEqual(self.mockedSendFunction.call_count, 4) def test_is_subdomain_with_no_ip(self): - domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY) - - with self.assertRaises(NameserverError): - domain.nameservers = [ - ("ns1.nameserversubdomain.gov",), - ("ns2.nameserversubdomain.gov",), - ] + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY) + with self.assertRaises(NameserverError): + domain.nameservers = [ + ("ns1.nameserversubdomain.gov",), + ("ns2.nameserversubdomain.gov",), + ] def test_not_subdomain_but_has_ip(self): - domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY) - - with self.assertRaises(NameserverError): - domain.nameservers = [ - ("ns1.cats-da-best.gov", ["1.2.3.4"]), - ("ns2.cats-da-best.gov", ["2.3.4.5"]), - ] + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY) + with self.assertRaises(NameserverError): + domain.nameservers = [ + ("ns1.cats-da-best.gov", ["1.2.3.4"]), + ("ns2.cats-da-best.gov", ["2.3.4.5"]), + ] def test_is_subdomain_but_ip_addr_not_valid(self): - domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="nameserversubdomain.gov", state=Domain.State.READY) - with self.assertRaises(NameserverError): - domain.nameservers = [ - ("ns1.nameserversubdomain.gov", ["1.2.3"]), - ("ns2.nameserversubdomain.gov", ["2.3.4"]), - ] + with self.assertRaises(NameserverError): + domain.nameservers = [ + ("ns1.nameserversubdomain.gov", ["1.2.3"]), + ("ns2.nameserversubdomain.gov", ["2.3.4"]), + ] def test_setting_not_allowed(self): """Scenario: A domain state is not Ready or DNS needed then setting nameservers is not allowed""" - domain, _ = Domain.objects.get_or_create(name="onholdDomain.gov", state=Domain.State.ON_HOLD) - with self.assertRaises(ActionNotAllowed): - domain.nameservers = [self.nameserver1, self.nameserver2] + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="onholdDomain.gov", state=Domain.State.ON_HOLD) + with self.assertRaises(ActionNotAllowed): + domain.nameservers = [self.nameserver1, self.nameserver2] def test_nameserver_returns_on_registry_error(self): """ @@ -1602,28 +1551,25 @@ class TestRegistrantNameservers(MockEppLib): Registry is unavailable and throws exception when attempting to build cache from registry. Nameservers retrieved from database. """ - domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) - # set the host and host_ips directly in the database; this is normally handled through - # fetch_cache - host, _ = Host.objects.get_or_create(domain=domain, name="ns1.fake.gov") - host_ip, _ = HostIP.objects.get_or_create(host=host, address="1.1.1.1") + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) + # set the host and host_ips directly in the database; this is normally handled through + # fetch_cache + host, _ = Host.objects.get_or_create(domain=domain, name="ns1.fake.gov") + host_ip, _ = HostIP.objects.get_or_create(host=host, address="1.1.1.1") - # mock that registry throws an error on the InfoHost send + # mock that registry throws an error on the InfoHost send + def side_effect(_request, cleaned): + raise RegistryError(code=ErrorCode.COMMAND_FAILED) - def side_effect(_request, cleaned): - raise RegistryError(code=ErrorCode.COMMAND_FAILED) - - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - nameservers = domain.nameservers - - self.assertEqual(len(nameservers), 1) - self.assertEqual(nameservers[0][0], "ns1.fake.gov") - self.assertEqual(nameservers[0][1], ["1.1.1.1"]) - - patcher.stop() + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + nameservers = domain.nameservers + self.assertEqual(len(nameservers), 1) + self.assertEqual(nameservers[0][0], "ns1.fake.gov") + self.assertEqual(nameservers[0][1], ["1.1.1.1"]) + patcher.stop() def test_nameservers_stored_on_fetch_cache(self): """ @@ -1633,24 +1579,23 @@ class TestRegistrantNameservers(MockEppLib): of 'fake.host.com' from InfoDomain and an array of 2 IPs: 1.2.3.4 and 2.3.4.5 from InfoHost """ - domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) - - # mock the get_or_create methods for Host and HostIP - with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object( - HostIP.objects, "get_or_create" - ) as mock_host_ip_get_or_create: - # Set the return value for the mocks - mock_host_get_or_create.return_value = (Host(), True) - mock_host_ip_get_or_create.return_value = (HostIP(), True) - - # force fetch_cache to be called, which will return above documented mocked hosts - domain.nameservers - # assert that the mocks are called - mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.host.com") - # Retrieve the mocked_host from the return value of the mock - actual_mocked_host, _ = mock_host_get_or_create.return_value - mock_host_ip_get_or_create.assert_called_with(address="2.3.4.5", host=actual_mocked_host) - self.assertEqual(mock_host_ip_get_or_create.call_count, 2) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) + # mock the get_or_create methods for Host and HostIP + with patch.object(Host.objects, "get_or_create") as mock_host_get_or_create, patch.object( + HostIP.objects, "get_or_create" + ) as mock_host_ip_get_or_create: + # Set the return value for the mocks + mock_host_get_or_create.return_value = (Host(), True) + mock_host_ip_get_or_create.return_value = (HostIP(), True) + # force fetch_cache to be called, which will return above documented mocked hosts + domain.nameservers + # assert that the mocks are called + mock_host_get_or_create.assert_called_once_with(domain=domain, name="fake.host.com") + # Retrieve the mocked_host from the return value of the mock + actual_mocked_host, _ = mock_host_get_or_create.return_value + mock_host_ip_get_or_create.assert_called_with(address="2.3.4.5", host=actual_mocked_host) + self.assertEqual(mock_host_ip_get_or_create.call_count, 2) @skip("not implemented yet") def test_update_is_unsuccessful(self): @@ -1791,54 +1736,51 @@ class TestRegistrantDNSSEC(MockEppLib): else: return MagicMock(res_data=[self.mockDataInfoHosts]) - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") - domain.dnssecdata = self.dnssecExtensionWithDsData - - # get the DNS SEC extension added to the UpdateDomain command and - # verify that it is properly sent - # args[0] is the _request sent to registry - args, _ = mocked_send.call_args - # assert that the extension on the update matches - self.assertEquals( - args[0].extensions[0], - self.createUpdateExtension(self.dnssecExtensionWithDsData), - ) - # test that the dnssecdata getter is functioning properly - dnssecdata_get = domain.dnssecdata - mocked_send.assert_has_calls( - [ - call( - commands.InfoDomain( - name="dnssec-dsdata.gov", + with less_console_noise(): + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") + domain.dnssecdata = self.dnssecExtensionWithDsData + # get the DNS SEC extension added to the UpdateDomain command and + # verify that it is properly sent + # args[0] is the _request sent to registry + args, _ = mocked_send.call_args + # assert that the extension on the update matches + self.assertEquals( + args[0].extensions[0], + self.createUpdateExtension(self.dnssecExtensionWithDsData), + ) + # test that the dnssecdata getter is functioning properly + dnssecdata_get = domain.dnssecdata + mocked_send.assert_has_calls( + [ + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, ), - cleaned=True, - ), - call( - commands.UpdateDomain( - name="dnssec-dsdata.gov", - nsset=None, - keyset=None, - registrant=None, - auth_info=None, + call( + commands.UpdateDomain( + name="dnssec-dsdata.gov", + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, ), - cleaned=True, - ), - call( - commands.InfoDomain( - name="dnssec-dsdata.gov", + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, ), - cleaned=True, - ), - ] - ) - - self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData) - - patcher.stop() + ] + ) + self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData) + patcher.stop() def test_dnssec_is_idempotent(self): """ @@ -1872,54 +1814,51 @@ class TestRegistrantDNSSEC(MockEppLib): else: return MagicMock(res_data=[self.mockDataInfoHosts]) - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") - - # set the dnssecdata once - domain.dnssecdata = self.dnssecExtensionWithDsData - # set the dnssecdata again - domain.dnssecdata = self.dnssecExtensionWithDsData - # test that the dnssecdata getter is functioning properly - dnssecdata_get = domain.dnssecdata - mocked_send.assert_has_calls( - [ - call( - commands.InfoDomain( - name="dnssec-dsdata.gov", + with less_console_noise(): + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") + # set the dnssecdata once + domain.dnssecdata = self.dnssecExtensionWithDsData + # set the dnssecdata again + domain.dnssecdata = self.dnssecExtensionWithDsData + # test that the dnssecdata getter is functioning properly + dnssecdata_get = domain.dnssecdata + mocked_send.assert_has_calls( + [ + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, ), - cleaned=True, - ), - call( - commands.UpdateDomain( - name="dnssec-dsdata.gov", - nsset=None, - keyset=None, - registrant=None, - auth_info=None, + call( + commands.UpdateDomain( + name="dnssec-dsdata.gov", + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, ), - cleaned=True, - ), - call( - commands.InfoDomain( - name="dnssec-dsdata.gov", + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, ), - cleaned=True, - ), - call( - commands.InfoDomain( - name="dnssec-dsdata.gov", + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, ), - cleaned=True, - ), - ] - ) - - self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData) - - patcher.stop() + ] + ) + self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithDsData.dsData) + patcher.stop() def test_user_adds_dnssec_data_multiple_dsdata(self): """ @@ -1949,48 +1888,45 @@ class TestRegistrantDNSSEC(MockEppLib): else: return MagicMock(res_data=[self.mockDataInfoHosts]) - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - domain, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov") - - domain.dnssecdata = self.dnssecExtensionWithMultDsData - # get the DNS SEC extension added to the UpdateDomain command - # and verify that it is properly sent - # args[0] is the _request sent to registry - args, _ = mocked_send.call_args - # assert that the extension matches - self.assertEquals( - args[0].extensions[0], - self.createUpdateExtension(self.dnssecExtensionWithMultDsData), - ) - # test that the dnssecdata getter is functioning properly - dnssecdata_get = domain.dnssecdata - mocked_send.assert_has_calls( - [ - call( - commands.UpdateDomain( - name="dnssec-multdsdata.gov", - nsset=None, - keyset=None, - registrant=None, - auth_info=None, + with less_console_noise(): + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + domain, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov") + domain.dnssecdata = self.dnssecExtensionWithMultDsData + # get the DNS SEC extension added to the UpdateDomain command + # and verify that it is properly sent + # args[0] is the _request sent to registry + args, _ = mocked_send.call_args + # assert that the extension matches + self.assertEquals( + args[0].extensions[0], + self.createUpdateExtension(self.dnssecExtensionWithMultDsData), + ) + # test that the dnssecdata getter is functioning properly + dnssecdata_get = domain.dnssecdata + mocked_send.assert_has_calls( + [ + call( + commands.UpdateDomain( + name="dnssec-multdsdata.gov", + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, ), - cleaned=True, - ), - call( - commands.InfoDomain( - name="dnssec-multdsdata.gov", + call( + commands.InfoDomain( + name="dnssec-multdsdata.gov", + ), + cleaned=True, ), - cleaned=True, - ), - ] - ) - - self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithMultDsData.dsData) - - patcher.stop() + ] + ) + self.assertEquals(dnssecdata_get.dsData, self.dnssecExtensionWithMultDsData.dsData) + patcher.stop() def test_user_removes_dnssec_data(self): """ @@ -2021,65 +1957,64 @@ class TestRegistrantDNSSEC(MockEppLib): else: return MagicMock(res_data=[self.mockDataInfoHosts]) - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") - # dnssecdata_get_initial = domain.dnssecdata # call to force initial mock - # domain._invalidate_cache() - domain.dnssecdata = self.dnssecExtensionWithDsData - domain.dnssecdata = self.dnssecExtensionRemovingDsData - # get the DNS SEC extension added to the UpdateDomain command and - # verify that it is properly sent - # args[0] is the _request sent to registry - args, _ = mocked_send.call_args - # assert that the extension on the update matches - self.assertEquals( - args[0].extensions[0], - self.createUpdateExtension( - self.dnssecExtensionWithDsData, - remove=True, - ), - ) - mocked_send.assert_has_calls( - [ - call( - commands.InfoDomain( - name="dnssec-dsdata.gov", - ), - cleaned=True, + with less_console_noise(): + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + domain, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") + # dnssecdata_get_initial = domain.dnssecdata # call to force initial mock + # domain._invalidate_cache() + domain.dnssecdata = self.dnssecExtensionWithDsData + domain.dnssecdata = self.dnssecExtensionRemovingDsData + # get the DNS SEC extension added to the UpdateDomain command and + # verify that it is properly sent + # args[0] is the _request sent to registry + args, _ = mocked_send.call_args + # assert that the extension on the update matches + self.assertEquals( + args[0].extensions[0], + self.createUpdateExtension( + self.dnssecExtensionWithDsData, + remove=True, ), - call( - commands.UpdateDomain( - name="dnssec-dsdata.gov", - nsset=None, - keyset=None, - registrant=None, - auth_info=None, + ) + mocked_send.assert_has_calls( + [ + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, ), - cleaned=True, - ), - call( - commands.InfoDomain( - name="dnssec-dsdata.gov", + call( + commands.UpdateDomain( + name="dnssec-dsdata.gov", + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, ), - cleaned=True, - ), - call( - commands.UpdateDomain( - name="dnssec-dsdata.gov", - nsset=None, - keyset=None, - registrant=None, - auth_info=None, + call( + commands.InfoDomain( + name="dnssec-dsdata.gov", + ), + cleaned=True, ), - cleaned=True, - ), - ] - ) - - patcher.stop() + call( + commands.UpdateDomain( + name="dnssec-dsdata.gov", + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ), + ] + ) + patcher.stop() def test_update_is_unsuccessful(self): """ @@ -2087,12 +2022,11 @@ class TestRegistrantDNSSEC(MockEppLib): When an error is returned from epplibwrapper Then a user-friendly error message is returned for displaying on the web """ - - domain, _ = Domain.objects.get_or_create(name="dnssec-invalid.gov") - - with self.assertRaises(RegistryError) as err: - domain.dnssecdata = self.dnssecExtensionWithDsData - self.assertTrue(err.is_client_error() or err.is_session_error() or err.is_server_error()) + with less_console_noise(): + domain, _ = Domain.objects.get_or_create(name="dnssec-invalid.gov") + with self.assertRaises(RegistryError) as err: + domain.dnssecdata = self.dnssecExtensionWithDsData + self.assertTrue(err.is_client_error() or err.is_session_error() or err.is_server_error()) class TestExpirationDate(MockEppLib): @@ -2117,44 +2051,49 @@ class TestExpirationDate(MockEppLib): def test_expiration_date_setter_not_implemented(self): """assert that the setter for expiration date is not implemented and will raise error""" - with self.assertRaises(NotImplementedError): - self.domain.registry_expiration_date = datetime.date.today() + with less_console_noise(): + with self.assertRaises(NotImplementedError): + self.domain.registry_expiration_date = datetime.date.today() def test_renew_domain(self): """assert that the renew_domain sets new expiration date in cache and saves to registrar""" - self.domain.renew_domain() - test_date = datetime.date(2023, 5, 25) - self.assertEquals(self.domain._cache["ex_date"], test_date) - self.assertEquals(self.domain.expiration_date, test_date) + with less_console_noise(): + self.domain.renew_domain() + test_date = datetime.date(2023, 5, 25) + self.assertEquals(self.domain._cache["ex_date"], test_date) + self.assertEquals(self.domain.expiration_date, test_date) def test_renew_domain_error(self): """assert that the renew_domain raises an exception when registry raises error""" - with self.assertRaises(RegistryError): - self.domain_w_error.renew_domain() + with less_console_noise(): + with self.assertRaises(RegistryError): + self.domain_w_error.renew_domain() def test_is_expired(self): """assert that is_expired returns true for expiration_date in past""" - # force fetch_cache to be called - self.domain.statuses - self.assertTrue(self.domain.is_expired) + with less_console_noise(): + # force fetch_cache to be called + self.domain.statuses + self.assertTrue(self.domain.is_expired) def test_is_not_expired(self): """assert that is_expired returns false for expiration in future""" - # to do this, need to mock value returned from timezone.now - # set now to 2023-01-01 - mocked_datetime = datetime.datetime(2023, 1, 1, 12, 0, 0) - # force fetch_cache which sets the expiration date to 2023-05-25 - self.domain.statuses - - with patch("registrar.models.domain.timezone.now", return_value=mocked_datetime): - self.assertFalse(self.domain.is_expired()) + with less_console_noise(): + # to do this, need to mock value returned from timezone.now + # set now to 2023-01-01 + mocked_datetime = datetime.datetime(2023, 1, 1, 12, 0, 0) + # force fetch_cache which sets the expiration date to 2023-05-25 + self.domain.statuses + with patch("registrar.models.domain.timezone.now", return_value=mocked_datetime): + self.assertFalse(self.domain.is_expired()) def test_expiration_date_updated_on_info_domain_call(self): """assert that expiration date in db is updated on info domain call""" - # force fetch_cache to be called - self.domain.statuses - test_date = datetime.date(2023, 5, 25) - self.assertEquals(self.domain.expiration_date, test_date) + with less_console_noise(): + # force fetch_cache to be called + self.domain.statuses + test_date = datetime.date(2023, 5, 25) + self.assertEquals(self.domain.expiration_date, test_date) class TestCreationDate(MockEppLib): @@ -2169,7 +2108,7 @@ class TestCreationDate(MockEppLib): self.domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY) # creation_date returned from mockDataInfoDomain with creation date: # cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35) - self.creation_date = datetime.datetime(2023, 5, 25, 19, 45, 35) + self.creation_date = make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)) def tearDown(self): Domain.objects.all().delete() @@ -2212,29 +2151,30 @@ class TestAnalystClientHold(MockEppLib): When `domain.place_client_hold()` is called Then `CLIENT_HOLD` is added to the domain's statuses """ - self.domain.place_client_hold() - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.UpdateDomain( - name="fake.gov", - add=[ - common.Status( - state=Domain.Status.CLIENT_HOLD, - description="", - lang="en", - ) - ], - nsset=None, - keyset=None, - registrant=None, - auth_info=None, - ), - cleaned=True, - ) - ] - ) - self.assertEquals(self.domain.state, Domain.State.ON_HOLD) + with less_console_noise(): + self.domain.place_client_hold() + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.UpdateDomain( + name="fake.gov", + add=[ + common.Status( + state=Domain.Status.CLIENT_HOLD, + description="", + lang="en", + ) + ], + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ) + ] + ) + self.assertEquals(self.domain.state, Domain.State.ON_HOLD) def test_analyst_places_client_hold_idempotent(self): """ @@ -2243,29 +2183,30 @@ class TestAnalystClientHold(MockEppLib): When `domain.place_client_hold()` is called Then Domain returns normally (without error) """ - self.domain_on_hold.place_client_hold() - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.UpdateDomain( - name="fake-on-hold.gov", - add=[ - common.Status( - state=Domain.Status.CLIENT_HOLD, - description="", - lang="en", - ) - ], - nsset=None, - keyset=None, - registrant=None, - auth_info=None, - ), - cleaned=True, - ) - ] - ) - self.assertEquals(self.domain_on_hold.state, Domain.State.ON_HOLD) + with less_console_noise(): + self.domain_on_hold.place_client_hold() + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.UpdateDomain( + name="fake-on-hold.gov", + add=[ + common.Status( + state=Domain.Status.CLIENT_HOLD, + description="", + lang="en", + ) + ], + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ) + ] + ) + self.assertEquals(self.domain_on_hold.state, Domain.State.ON_HOLD) def test_analyst_removes_client_hold(self): """ @@ -2274,29 +2215,30 @@ class TestAnalystClientHold(MockEppLib): When `domain.remove_client_hold()` is called Then `CLIENT_HOLD` is no longer in the domain's statuses """ - self.domain_on_hold.revert_client_hold() - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.UpdateDomain( - name="fake-on-hold.gov", - rem=[ - common.Status( - state=Domain.Status.CLIENT_HOLD, - description="", - lang="en", - ) - ], - nsset=None, - keyset=None, - registrant=None, - auth_info=None, - ), - cleaned=True, - ) - ] - ) - self.assertEquals(self.domain_on_hold.state, Domain.State.READY) + with less_console_noise(): + self.domain_on_hold.revert_client_hold() + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.UpdateDomain( + name="fake-on-hold.gov", + rem=[ + common.Status( + state=Domain.Status.CLIENT_HOLD, + description="", + lang="en", + ) + ], + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ) + ] + ) + self.assertEquals(self.domain_on_hold.state, Domain.State.READY) def test_analyst_removes_client_hold_idempotent(self): """ @@ -2305,29 +2247,30 @@ class TestAnalystClientHold(MockEppLib): When `domain.remove_client_hold()` is called Then Domain returns normally (without error) """ - self.domain.revert_client_hold() - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.UpdateDomain( - name="fake.gov", - rem=[ - common.Status( - state=Domain.Status.CLIENT_HOLD, - description="", - lang="en", - ) - ], - nsset=None, - keyset=None, - registrant=None, - auth_info=None, - ), - cleaned=True, - ) - ] - ) - self.assertEquals(self.domain.state, Domain.State.READY) + with less_console_noise(): + self.domain.revert_client_hold() + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.UpdateDomain( + name="fake.gov", + rem=[ + common.Status( + state=Domain.Status.CLIENT_HOLD, + description="", + lang="en", + ) + ], + nsset=None, + keyset=None, + registrant=None, + auth_info=None, + ), + cleaned=True, + ) + ] + ) + self.assertEquals(self.domain.state, Domain.State.READY) def test_update_is_unsuccessful(self): """ @@ -2339,18 +2282,17 @@ class TestAnalystClientHold(MockEppLib): def side_effect(_request, cleaned): raise RegistryError(code=ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION) - patcher = patch("registrar.models.domain.registry.send") - mocked_send = patcher.start() - mocked_send.side_effect = side_effect - - # if RegistryError is raised, admin formats user-friendly - # error message if error is_client_error, is_session_error, or - # is_server_error; so test for those conditions - with self.assertRaises(RegistryError) as err: - self.domain.place_client_hold() - self.assertTrue(err.is_client_error() or err.is_session_error() or err.is_server_error()) - - patcher.stop() + with less_console_noise(): + patcher = patch("registrar.models.domain.registry.send") + mocked_send = patcher.start() + mocked_send.side_effect = side_effect + # if RegistryError is raised, admin formats user-friendly + # error message if error is_client_error, is_session_error, or + # is_server_error; so test for those conditions + with self.assertRaises(RegistryError) as err: + self.domain.place_client_hold() + self.assertTrue(err.is_client_error() or err.is_session_error() or err.is_server_error()) + patcher.stop() class TestAnalystLock(TestCase): @@ -2443,31 +2385,28 @@ class TestAnalystDelete(MockEppLib): The deleted date is set. """ - # Put the domain in client hold - self.domain.place_client_hold() - # Delete it... - self.domain.deletedInEpp() - self.domain.save() - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.DeleteDomain(name="fake.gov"), - cleaned=True, - ) - ] - ) - - # Domain itself should not be deleted - self.assertNotEqual(self.domain, None) - - # Domain should have the right state - self.assertEqual(self.domain.state, Domain.State.DELETED) - - # Domain should have a deleted - self.assertNotEqual(self.domain.deleted, None) - - # Cache should be invalidated - self.assertEqual(self.domain._cache, {}) + with less_console_noise(): + # Put the domain in client hold + self.domain.place_client_hold() + # Delete it... + self.domain.deletedInEpp() + self.domain.save() + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.DeleteDomain(name="fake.gov"), + cleaned=True, + ) + ] + ) + # Domain itself should not be deleted + self.assertNotEqual(self.domain, None) + # Domain should have the right state + self.assertEqual(self.domain.state, Domain.State.DELETED) + # Domain should have a deleted + self.assertNotEqual(self.domain.deleted, None) + # Cache should be invalidated + self.assertEqual(self.domain._cache, {}) def test_deletion_is_unsuccessful(self): """ @@ -2476,29 +2415,28 @@ class TestAnalystDelete(MockEppLib): Then a client error is returned of code 2305 And `state` is not set to `DELETED` """ - # Desired domain - domain, _ = Domain.objects.get_or_create(name="failDelete.gov", state=Domain.State.ON_HOLD) - # Put the domain in client hold - domain.place_client_hold() - - # Delete it - with self.assertRaises(RegistryError) as err: - domain.deletedInEpp() - domain.save() - self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION) - self.mockedSendFunction.assert_has_calls( - [ - call( - commands.DeleteDomain(name="failDelete.gov"), - cleaned=True, - ) - ] - ) - - # Domain itself should not be deleted - self.assertNotEqual(domain, None) - # State should not have changed - self.assertEqual(domain.state, Domain.State.ON_HOLD) + with less_console_noise(): + # Desired domain + domain, _ = Domain.objects.get_or_create(name="failDelete.gov", state=Domain.State.ON_HOLD) + # Put the domain in client hold + domain.place_client_hold() + # Delete it + with self.assertRaises(RegistryError) as err: + domain.deletedInEpp() + domain.save() + self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION) + self.mockedSendFunction.assert_has_calls( + [ + call( + commands.DeleteDomain(name="failDelete.gov"), + cleaned=True, + ) + ] + ) + # Domain itself should not be deleted + self.assertNotEqual(domain, None) + # State should not have changed + self.assertEqual(domain.state, Domain.State.ON_HOLD) def test_deletion_ready_fsm_failure(self): """ @@ -2511,15 +2449,15 @@ class TestAnalystDelete(MockEppLib): The deleted date is still null. """ - self.assertEqual(self.domain.state, Domain.State.READY) - with self.assertRaises(TransitionNotAllowed) as err: - self.domain.deletedInEpp() - self.domain.save() - self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION) - # Domain should not be deleted - self.assertNotEqual(self.domain, None) - # Domain should have the right state - self.assertEqual(self.domain.state, Domain.State.READY) - - # deleted should be null - self.assertEqual(self.domain.deleted, None) + with less_console_noise(): + self.assertEqual(self.domain.state, Domain.State.READY) + with self.assertRaises(TransitionNotAllowed) as err: + self.domain.deletedInEpp() + self.domain.save() + self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION) + # Domain should not be deleted + self.assertNotEqual(self.domain, None) + # Domain should have the right state + self.assertEqual(self.domain.state, Domain.State.READY) + # deleted should be null + self.assertEqual(self.domain.deleted, None) diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py index a85fb5849..630904218 100644 --- a/src/registrar/tests/test_reports.py +++ b/src/registrar/tests/test_reports.py @@ -23,6 +23,7 @@ import boto3_mocking from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # type: ignore from datetime import date, datetime, timedelta from django.utils import timezone +from .common import less_console_noise class CsvReportsTest(TestCase): @@ -80,41 +81,43 @@ class CsvReportsTest(TestCase): @boto3_mocking.patching def test_generate_federal_report(self): """Ensures that we correctly generate current-federal.csv""" - mock_client = MagicMock() - fake_open = mock_open() - expected_file_content = [ - call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\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. - with boto3_mocking.clients.handler_for("s3", mock_client): - with patch("builtins.open", fake_open): - call_command("generate_current_federal_report", checkpath=False) - content = fake_open() + with less_console_noise(): + mock_client = MagicMock() + fake_open = mock_open() + expected_file_content = [ + call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\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. + with boto3_mocking.clients.handler_for("s3", mock_client): + with patch("builtins.open", 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 def test_generate_full_report(self): """Ensures that we correctly generate current-full.csv""" - mock_client = MagicMock() - fake_open = mock_open() - expected_file_content = [ - call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"), - call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \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. - with boto3_mocking.clients.handler_for("s3", mock_client): - with patch("builtins.open", fake_open): - call_command("generate_current_full_report", checkpath=False) - content = fake_open() + with less_console_noise(): + mock_client = MagicMock() + fake_open = mock_open() + expected_file_content = [ + call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"), + call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \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. + with boto3_mocking.clients.handler_for("s3", mock_client): + with patch("builtins.open", 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 def test_not_found_full_report(self): @@ -123,19 +126,20 @@ class CsvReportsTest(TestCase): def side_effect(Bucket, Key): raise ClientError({"Error": {"Code": "NoSuchKey", "Message": "No such key"}}, "get_object") - mock_client = MagicMock() - mock_client.get_object.side_effect = side_effect + with less_console_noise(): + mock_client = MagicMock() + mock_client.get_object.side_effect = side_effect - response = None - with boto3_mocking.clients.handler_for("s3", mock_client): - with patch("boto3.client", return_value=mock_client): - with self.assertRaises(S3ClientError) as context: - response = self.client.get("/api/v1/get-report/current-full") - # Check that the response has status code 500 - self.assertEqual(response.status_code, 500) + response = None + with boto3_mocking.clients.handler_for("s3", mock_client): + with patch("boto3.client", return_value=mock_client): + with self.assertRaises(S3ClientError) as context: + response = self.client.get("/api/v1/get-report/current-full") + # Check that the response has status code 500 + self.assertEqual(response.status_code, 500) - # Check that we get the right error back from the page - self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR) + # Check that we get the right error back from the page + self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR) @boto3_mocking.patching def test_not_found_federal_report(self): @@ -144,83 +148,86 @@ class CsvReportsTest(TestCase): def side_effect(Bucket, Key): raise ClientError({"Error": {"Code": "NoSuchKey", "Message": "No such key"}}, "get_object") - mock_client = MagicMock() - mock_client.get_object.side_effect = side_effect + with less_console_noise(): + mock_client = MagicMock() + mock_client.get_object.side_effect = side_effect - with boto3_mocking.clients.handler_for("s3", mock_client): - with patch("boto3.client", return_value=mock_client): - with self.assertRaises(S3ClientError) as context: - response = self.client.get("/api/v1/get-report/current-federal") - # Check that the response has status code 500 - self.assertEqual(response.status_code, 500) + with boto3_mocking.clients.handler_for("s3", mock_client): + with patch("boto3.client", return_value=mock_client): + with self.assertRaises(S3ClientError) as context: + response = self.client.get("/api/v1/get-report/current-federal") + # Check that the response has status code 500 + self.assertEqual(response.status_code, 500) - # Check that we get the right error back from the page - self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR) + # Check that we get the right error back from the page + self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR) @boto3_mocking.patching def test_load_federal_report(self): """Tests the get_current_federal api endpoint""" - mock_client = MagicMock() - mock_client_instance = mock_client.return_value + with less_console_noise(): + mock_client = MagicMock() + mock_client_instance = mock_client.return_value - with open("registrar/tests/data/fake_current_federal.csv", "r") as file: - file_content = file.read() + with open("registrar/tests/data/fake_current_federal.csv", "r") as file: + file_content = file.read() - # Mock a recieved file - mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())} - with boto3_mocking.clients.handler_for("s3", mock_client): - request = self.factory.get("/fake-path") - response = get_current_federal(request) + # Mock a recieved file + mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())} + with boto3_mocking.clients.handler_for("s3", mock_client): + request = self.factory.get("/fake-path") + response = get_current_federal(request) - # Check that we are sending the correct calls. - # 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")] - mock_client_instance.assert_has_calls(expected_call) + # Check that we are sending the correct calls. + # 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")] + mock_client_instance.assert_has_calls(expected_call) - # Check that the response has status code 200 - self.assertEqual(response.status_code, 200) + # Check that the response has status code 200 + self.assertEqual(response.status_code, 200) - # Check that the response contains what we expect - expected_file_content = ( - "Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n" - "cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\n" - "ddomain3.gov,Federal,Armed Forces Retirement Home,,,," - ).encode() + # Check that the response contains what we expect + expected_file_content = ( + "Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n" + "cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\n" + "ddomain3.gov,Federal,Armed Forces Retirement Home,,,," + ).encode() - self.assertEqual(expected_file_content, response.content) + self.assertEqual(expected_file_content, response.content) @boto3_mocking.patching def test_load_full_report(self): """Tests the current-federal api link""" - mock_client = MagicMock() - mock_client_instance = mock_client.return_value + with less_console_noise(): + mock_client = MagicMock() + mock_client_instance = mock_client.return_value - with open("registrar/tests/data/fake_current_full.csv", "r") as file: - file_content = file.read() + with open("registrar/tests/data/fake_current_full.csv", "r") as file: + file_content = file.read() - # Mock a recieved file - mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())} - with boto3_mocking.clients.handler_for("s3", mock_client): - request = self.factory.get("/fake-path") - response = get_current_full(request) + # Mock a recieved file + mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())} + with boto3_mocking.clients.handler_for("s3", mock_client): + request = self.factory.get("/fake-path") + response = get_current_full(request) - # Check that we are sending the correct calls. - # 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")] - mock_client_instance.assert_has_calls(expected_call) + # Check that we are sending the correct calls. + # 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")] + mock_client_instance.assert_has_calls(expected_call) - # Check that the response has status code 200 - self.assertEqual(response.status_code, 200) + # Check that the response has status code 200 + self.assertEqual(response.status_code, 200) - # Check that the response contains what we expect - expected_file_content = ( - "Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n" - "cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\n" - "ddomain3.gov,Federal,Armed Forces Retirement Home,,,,\n" - "adomain2.gov,Interstate,,,,," - ).encode() + # Check that the response contains what we expect + expected_file_content = ( + "Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n" + "cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\n" + "ddomain3.gov,Federal,Armed Forces Retirement Home,,,,\n" + "adomain2.gov,Interstate,,,,," + ).encode() - self.assertEqual(expected_file_content, response.content) + self.assertEqual(expected_file_content, response.content) class ExportDataTest(MockEppLib): @@ -339,192 +346,170 @@ class ExportDataTest(MockEppLib): def test_export_domains_to_writer_security_emails(self): """Test that export_domains_to_writer returns the expected security email""" - - # Add security email information - self.domain_1.name = "defaultsecurity.gov" - self.domain_1.save() - - # Invoke setter - self.domain_1.security_contact - - # Invoke setter - self.domain_2.security_contact - - # Invoke setter - self.domain_3.security_contact - - # Create a CSV file in memory - csv_file = StringIO() - writer = csv.writer(csv_file) - - # Define columns, sort fields, and filter condition - columns = [ - "Domain name", - "Domain type", - "Agency", - "Organization name", - "City", - "State", - "AO", - "AO email", - "Security contact email", - "Status", - "Expiration date", - ] - sort_fields = ["domain__name"] - filter_condition = { - "domain__state__in": [ - Domain.State.READY, - Domain.State.DNS_NEEDED, - Domain.State.ON_HOLD, - ], - } - - self.maxDiff = None - # Call the export functions - write_header(writer, columns) - write_body(writer, columns, sort_fields, filter_condition) - - # Reset the CSV file's position to the beginning - csv_file.seek(0) - - # Read the content into a variable - csv_content = csv_file.read() - - # We expect READY domains, - # sorted alphabetially by domain name - expected_content = ( - "Domain name,Domain type,Agency,Organization name,City,State,AO," - "AO email,Security contact email,Status,Expiration date\n" - "adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n" - "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) + with less_console_noise(): + # Add security email information + self.domain_1.name = "defaultsecurity.gov" + self.domain_1.save() + # Invoke setter + self.domain_1.security_contact + # Invoke setter + self.domain_2.security_contact + # Invoke setter + self.domain_3.security_contact + # Create a CSV file in memory + csv_file = StringIO() + writer = csv.writer(csv_file) + # Define columns, sort fields, and filter condition + columns = [ + "Domain name", + "Domain type", + "Agency", + "Organization name", + "City", + "State", + "AO", + "AO email", + "Security contact email", + "Status", + "Expiration date", + ] + sort_fields = ["domain__name"] + filter_condition = { + "domain__state__in": [ + Domain.State.READY, + Domain.State.DNS_NEEDED, + Domain.State.ON_HOLD, + ], + } + self.maxDiff = None + # Call the export functions + write_header(writer, columns) + write_body(writer, columns, sort_fields, filter_condition) + # Reset the CSV file's position to the beginning + csv_file.seek(0) + # Read the content into a variable + csv_content = csv_file.read() + # We expect READY domains, + # sorted alphabetially by domain name + expected_content = ( + "Domain name,Domain type,Agency,Organization name,City,State,AO," + "AO email,Security contact email,Status,Expiration date\n" + "adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n" + "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): """Test that write_body returns the existing domain, test that sort by domain name works, test that filter works""" - # Create a CSV file in memory - csv_file = StringIO() - writer = csv.writer(csv_file) + with less_console_noise(): + # Create a CSV file in memory + csv_file = StringIO() + writer = csv.writer(csv_file) - # Define columns, sort fields, and filter condition - columns = [ - "Domain name", - "Domain type", - "Agency", - "Organization name", - "City", - "State", - "AO", - "AO email", - "Submitter", - "Submitter title", - "Submitter email", - "Submitter phone", - "Security contact email", - "Status", - ] - sort_fields = ["domain__name"] - filter_condition = { - "domain__state__in": [ - Domain.State.READY, - Domain.State.DNS_NEEDED, - Domain.State.ON_HOLD, - ], - } - - # Call the export functions - 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) - - # Read the content into a variable - csv_content = csv_file.read() - - # We expect READY domains, - # sorted alphabetially by domain name - expected_content = ( - "Domain name,Domain type,Agency,Organization name,City,State,AO," - "AO email,Submitter,Submitter title,Submitter email,Submitter phone," - "Security contact email,Status\n" - "adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n" - "adomain2.gov,Interstate,Dns needed\n" - "cdomain1.gov,Federal - Executive,World War I Centennial Commission,Ready\n" - "ddomain3.gov,Federal,Armed Forces Retirement Home,On hold\n" - ) - - # Normalize line endings and remove commas, - # spaces and leading/trailing whitespace - csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() - expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() - - self.assertEqual(csv_content, expected_content) + # Define columns, sort fields, and filter condition + columns = [ + "Domain name", + "Domain type", + "Agency", + "Organization name", + "City", + "State", + "AO", + "AO email", + "Submitter", + "Submitter title", + "Submitter email", + "Submitter phone", + "Security contact email", + "Status", + ] + sort_fields = ["domain__name"] + filter_condition = { + "domain__state__in": [ + Domain.State.READY, + Domain.State.DNS_NEEDED, + Domain.State.ON_HOLD, + ], + } + # Call the export functions + 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) + # Read the content into a variable + csv_content = csv_file.read() + # We expect READY domains, + # sorted alphabetially by domain name + expected_content = ( + "Domain name,Domain type,Agency,Organization name,City,State,AO," + "AO email,Submitter,Submitter title,Submitter email,Submitter phone," + "Security contact email,Status\n" + "adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n" + "adomain2.gov,Interstate,Dns needed\n" + "cdomain1.gov,Federal - Executive,World War I Centennial Commission,Ready\n" + "ddomain3.gov,Federal,Armed Forces Retirement Home,On hold\n" + ) + # Normalize line endings and remove commas, + # spaces and leading/trailing whitespace + csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() + expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() + self.assertEqual(csv_content, expected_content) def test_write_body_additional(self): """An additional test for filters and multi-column sort""" - # Create a CSV file in memory - csv_file = StringIO() - writer = csv.writer(csv_file) - - # Define columns, sort fields, and filter condition - columns = [ - "Domain name", - "Domain type", - "Agency", - "Organization name", - "City", - "State", - "Security contact email", - ] - sort_fields = ["domain__name", "federal_agency", "organization_type"] - filter_condition = { - "organization_type__icontains": "federal", - "domain__state__in": [ - Domain.State.READY, - Domain.State.DNS_NEEDED, - Domain.State.ON_HOLD, - ], - } - - # Call the export functions - 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) - - # Read the content into a variable - csv_content = csv_file.read() - - # We expect READY domains, - # federal only - # sorted alphabetially by domain name - expected_content = ( - "Domain name,Domain type,Agency,Organization name,City," - "State,Security contact email\n" - "adomain10.gov,Federal,Armed Forces Retirement Home\n" - "cdomain1.gov,Federal - Executive,World War I Centennial Commission\n" - "ddomain3.gov,Federal,Armed Forces Retirement Home\n" - ) - - # Normalize line endings and remove commas, - # spaces and leading/trailing whitespace - csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() - expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() - - self.assertEqual(csv_content, expected_content) + with less_console_noise(): + # Create a CSV file in memory + csv_file = StringIO() + writer = csv.writer(csv_file) + # Define columns, sort fields, and filter condition + columns = [ + "Domain name", + "Domain type", + "Agency", + "Organization name", + "City", + "State", + "Security contact email", + ] + sort_fields = ["domain__name", "federal_agency", "organization_type"] + filter_condition = { + "organization_type__icontains": "federal", + "domain__state__in": [ + Domain.State.READY, + Domain.State.DNS_NEEDED, + Domain.State.ON_HOLD, + ], + } + # Call the export functions + 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) + # Read the content into a variable + csv_content = csv_file.read() + # We expect READY domains, + # federal only + # sorted alphabetially by domain name + expected_content = ( + "Domain name,Domain type,Agency,Organization name,City," + "State,Security contact email\n" + "adomain10.gov,Federal,Armed Forces Retirement Home\n" + "cdomain1.gov,Federal - Executive,World War I Centennial Commission\n" + "ddomain3.gov,Federal,Armed Forces Retirement Home\n" + ) + # Normalize line endings and remove commas, + # spaces and leading/trailing whitespace + csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip() + expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip() + self.assertEqual(csv_content, expected_content) def test_write_body_with_date_filter_pulls_domains_in_range(self): """Test that domains that are @@ -538,88 +523,88 @@ class ExportDataTest(MockEppLib): which are hard to mock. 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 - 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())) + # Define columns, sort fields, and filter condition + columns = [ + "Domain name", + "Domain type", + "Agency", + "Organization name", + "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 - columns = [ - "Domain name", - "Domain type", - "Agency", - "Organization name", - "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, - } + # Call the export functions + write_header(writer, columns) + write_body( + writer, + columns, + sort_fields, + filter_condition, + ) + write_body( + writer, + columns, + sort_fields_for_deleted_domains, + filter_conditions_for_deleted_domains, + ) - # Call the export functions - write_header(writer, columns) - 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 + csv_file.seek(0) - # Reset the CSV file's position to the beginning - csv_file.seek(0) + # Read the content into a variable + csv_content = csv_file.read() - # Read the content into a variable - csv_content = csv_file.read() + # We expect READY domains first, created between today-2 and today+2, sorted by created_at then name + # 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 - # 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" - ) + # 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() - # 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) + self.assertEqual(csv_content, expected_content) class HelperFunctions(TestCase): diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py index be4619e0b..e9453bd03 100644 --- a/src/registrar/tests/test_transition_domain_migrations.py +++ b/src/registrar/tests/test_transition_domain_migrations.py @@ -20,6 +20,9 @@ from registrar.models.contact import Contact from .common import MockSESClient, less_console_noise import boto3_mocking # type: ignore +import logging + +logger = logging.getLogger(__name__) class TestProcessedMigrations(TestCase): @@ -55,17 +58,18 @@ class TestProcessedMigrations(TestCase): The 'call_command' function from Django's management framework is then used to execute the load_transition_domain command with the specified arguments. """ - # noqa here because splitting this up makes it confusing. - # ES501 - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command( - "load_transition_domain", - self.migration_json_filename, - directory=self.test_data_file_location, - ) + with less_console_noise(): + # noqa here because splitting this up makes it confusing. + # ES501 + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + call_command( + "load_transition_domain", + self.migration_json_filename, + directory=self.test_data_file_location, + ) def run_transfer_domains(self): """ @@ -74,101 +78,104 @@ class TestProcessedMigrations(TestCase): The 'call_command' function from Django's management framework is then used to execute the load_transition_domain command with the specified arguments. """ - call_command("transfer_transition_domains_to_domains") + with less_console_noise(): + call_command("transfer_transition_domains_to_domains") def test_domain_idempotent(self): """ This test ensures that the domain transfer process is idempotent on Domain and DomainInformation. """ - unchanged_domain, _ = Domain.objects.get_or_create( - name="testdomain.gov", - state=Domain.State.READY, - expiration_date=datetime.date(2000, 1, 1), - ) - unchanged_domain_information, _ = DomainInformation.objects.get_or_create( - domain=unchanged_domain, organization_name="test org name", creator=self.user - ) - self.run_load_domains() + with less_console_noise(): + unchanged_domain, _ = Domain.objects.get_or_create( + name="testdomain.gov", + state=Domain.State.READY, + expiration_date=datetime.date(2000, 1, 1), + ) + unchanged_domain_information, _ = DomainInformation.objects.get_or_create( + domain=unchanged_domain, organization_name="test org name", creator=self.user + ) + self.run_load_domains() - # Test that a given TransitionDomain isn't set to "processed" - transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") - self.assertFalse(transition_domain_object.processed) + # Test that a given TransitionDomain isn't set to "processed" + transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") + self.assertFalse(transition_domain_object.processed) - self.run_transfer_domains() + self.run_transfer_domains() - # Test that old data isn't corrupted - actual_unchanged = Domain.objects.filter(name="testdomain.gov").get() - actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get() - self.assertEqual(unchanged_domain, actual_unchanged) - self.assertEqual(unchanged_domain_information, actual_unchanged_information) + # Test that old data isn't corrupted + actual_unchanged = Domain.objects.filter(name="testdomain.gov").get() + actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get() + self.assertEqual(unchanged_domain, actual_unchanged) + self.assertEqual(unchanged_domain_information, actual_unchanged_information) - # Test that a given TransitionDomain is set to "processed" after we transfer domains - transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") - self.assertTrue(transition_domain_object.processed) + # Test that a given TransitionDomain is set to "processed" after we transfer domains + transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") + self.assertTrue(transition_domain_object.processed) - # Manually change Domain/DomainInformation objects - changed_domain = Domain.objects.filter(name="fakewebsite3.gov").get() - changed_domain.expiration_date = datetime.date(1999, 1, 1) + # Manually change Domain/DomainInformation objects + changed_domain = Domain.objects.filter(name="fakewebsite3.gov").get() + changed_domain.expiration_date = datetime.date(1999, 1, 1) - changed_domain.save() + changed_domain.save() - changed_domain_information = DomainInformation.objects.filter(domain=changed_domain).get() - changed_domain_information.organization_name = "changed" + changed_domain_information = DomainInformation.objects.filter(domain=changed_domain).get() + changed_domain_information.organization_name = "changed" - changed_domain_information.save() + changed_domain_information.save() - # Rerun transfer domains - self.run_transfer_domains() + # Rerun transfer domains + self.run_transfer_domains() - # Test that old data isn't corrupted after running this twice - actual_unchanged = Domain.objects.filter(name="testdomain.gov").get() - actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get() - self.assertEqual(unchanged_domain, actual_unchanged) - self.assertEqual(unchanged_domain_information, actual_unchanged_information) + # Test that old data isn't corrupted after running this twice + actual_unchanged = Domain.objects.filter(name="testdomain.gov").get() + actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get() + self.assertEqual(unchanged_domain, actual_unchanged) + self.assertEqual(unchanged_domain_information, actual_unchanged_information) - # Ensure that domain hasn't changed - actual_domain = Domain.objects.filter(name="fakewebsite3.gov").get() - self.assertEqual(changed_domain, actual_domain) + # Ensure that domain hasn't changed + actual_domain = Domain.objects.filter(name="fakewebsite3.gov").get() + self.assertEqual(changed_domain, actual_domain) - # Ensure that DomainInformation hasn't changed - actual_domain_information = DomainInformation.objects.filter(domain=changed_domain).get() - self.assertEqual(changed_domain_information, actual_domain_information) + # Ensure that DomainInformation hasn't changed + actual_domain_information = DomainInformation.objects.filter(domain=changed_domain).get() + self.assertEqual(changed_domain_information, actual_domain_information) def test_transition_domain_is_processed(self): """ This test checks if a domain is correctly marked as processed in the transition. """ - old_transition_domain, _ = TransitionDomain.objects.get_or_create(domain_name="testdomain.gov") - # Asser that old records default to 'True' - self.assertTrue(old_transition_domain.processed) + with less_console_noise(): + old_transition_domain, _ = TransitionDomain.objects.get_or_create(domain_name="testdomain.gov") + # Asser that old records default to 'True' + self.assertTrue(old_transition_domain.processed) - unchanged_domain, _ = Domain.objects.get_or_create( - name="testdomain.gov", - state=Domain.State.READY, - expiration_date=datetime.date(2000, 1, 1), - ) - unchanged_domain_information, _ = DomainInformation.objects.get_or_create( - domain=unchanged_domain, organization_name="test org name", creator=self.user - ) - self.run_load_domains() + unchanged_domain, _ = Domain.objects.get_or_create( + name="testdomain.gov", + state=Domain.State.READY, + expiration_date=datetime.date(2000, 1, 1), + ) + unchanged_domain_information, _ = DomainInformation.objects.get_or_create( + domain=unchanged_domain, organization_name="test org name", creator=self.user + ) + self.run_load_domains() - # Test that a given TransitionDomain isn't set to "processed" - transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") - self.assertFalse(transition_domain_object.processed) + # Test that a given TransitionDomain isn't set to "processed" + transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") + self.assertFalse(transition_domain_object.processed) - self.run_transfer_domains() + self.run_transfer_domains() - # Test that old data isn't corrupted - actual_unchanged = Domain.objects.filter(name="testdomain.gov").get() - actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get() - self.assertEqual(unchanged_domain, actual_unchanged) - self.assertTrue(old_transition_domain.processed) - self.assertEqual(unchanged_domain_information, actual_unchanged_information) + # Test that old data isn't corrupted + actual_unchanged = Domain.objects.filter(name="testdomain.gov").get() + actual_unchanged_information = DomainInformation.objects.filter(domain=actual_unchanged).get() + self.assertEqual(unchanged_domain, actual_unchanged) + self.assertTrue(old_transition_domain.processed) + self.assertEqual(unchanged_domain_information, actual_unchanged_information) - # Test that a given TransitionDomain is set to "processed" after we transfer domains - transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") - self.assertTrue(transition_domain_object.processed) + # Test that a given TransitionDomain is set to "processed" after we transfer domains + transition_domain_object = TransitionDomain.objects.get(domain_name="fakewebsite3.gov") + self.assertTrue(transition_domain_object.processed) class TestOrganizationMigration(TestCase): @@ -200,17 +207,18 @@ class TestOrganizationMigration(TestCase): The 'call_command' function from Django's management framework is then used to execute the load_transition_domain command with the specified arguments. """ - # noqa here because splitting this up makes it confusing. - # ES501 - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command( - "load_transition_domain", - self.migration_json_filename, - directory=self.test_data_file_location, - ) + with less_console_noise(): + # noqa here because splitting this up makes it confusing. + # ES501 + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + call_command( + "load_transition_domain", + self.migration_json_filename, + directory=self.test_data_file_location, + ) def run_transfer_domains(self): """ @@ -219,7 +227,8 @@ class TestOrganizationMigration(TestCase): The 'call_command' function from Django's management framework is then used to execute the load_transition_domain command with the specified arguments. """ - call_command("transfer_transition_domains_to_domains") + with less_console_noise(): + call_command("transfer_transition_domains_to_domains") def run_load_organization_data(self): """ @@ -232,17 +241,18 @@ class TestOrganizationMigration(TestCase): The 'call_command' function from Django's management framework is then used to execute the load_organization_data command with the specified arguments. """ - # noqa here (E501) because splitting this up makes it - # confusing to read. - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command( - "load_organization_data", - self.migration_json_filename, - directory=self.test_data_file_location, - ) + with less_console_noise(): + # noqa here (E501) because splitting this up makes it + # confusing to read. + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + call_command( + "load_organization_data", + self.migration_json_filename, + directory=self.test_data_file_location, + ) def compare_tables( self, @@ -256,7 +266,6 @@ class TestOrganizationMigration(TestCase): """Does a diff between the transition_domain and the following tables: domain, domain_information and the domain_invitation. Verifies that the data loaded correctly.""" - missing_domains = [] duplicate_domains = [] missing_domain_informations = [] @@ -300,59 +309,64 @@ class TestOrganizationMigration(TestCase): 3. Checks that the data has been loaded as expected. The expected result is a set of TransitionDomain objects with specific attributes. - The test fetches the actual TransitionDomain objects from the database and compares them with the expected objects. - """ # noqa - E501 (harder to read) - # == First, parse all existing data == # - self.run_load_domains() - self.run_transfer_domains() + The test fetches the actual TransitionDomain objects from the database and compares them with + the expected objects. + """ + with less_console_noise(): + # noqa - E501 (harder to read) + # == First, parse all existing data == # + self.run_load_domains() + self.run_transfer_domains() - # == Second, try adding org data to it == # - self.run_load_organization_data() + # == Second, try adding org data to it == # + self.run_load_organization_data() - # == Third, test that we've loaded data as we expect == # - transition_domains = TransitionDomain.objects.filter(domain_name="fakewebsite2.gov") + # == Third, test that we've loaded data as we expect == # + transition_domains = TransitionDomain.objects.filter(domain_name="fakewebsite2.gov") - # Should return three objects (three unique emails) - self.assertEqual(transition_domains.count(), 3) + # Should return three objects (three unique emails) + self.assertEqual(transition_domains.count(), 3) - # Lets test the first one - transition = transition_domains.first() - expected_transition_domain = TransitionDomain( - username="alexandra.bobbitt5@test.com", - domain_name="fakewebsite2.gov", - status="on hold", - email_sent=True, - organization_type="Federal", - organization_name="Fanoodle", - federal_type="Executive", - federal_agency="Department of Commerce", - epp_creation_date=datetime.date(2004, 5, 7), - epp_expiration_date=datetime.date(2023, 9, 30), - first_name="Seline", - middle_name="testmiddle2", - last_name="Tower", - title=None, - email="stower3@answers.com", - phone="151-539-6028", - address_line="93001 Arizona Drive", - city="Columbus", - state_territory="Oh", - zipcode="43268", - ) - expected_transition_domain.id = transition.id + # Lets test the first one + transition = transition_domains.first() + expected_transition_domain = TransitionDomain( + username="alexandra.bobbitt5@test.com", + domain_name="fakewebsite2.gov", + status="on hold", + email_sent=True, + organization_type="Federal", + organization_name="Fanoodle", + federal_type="Executive", + federal_agency="Department of Commerce", + epp_creation_date=datetime.date(2004, 5, 7), + epp_expiration_date=datetime.date(2023, 9, 30), + first_name="Seline", + middle_name="testmiddle2", + last_name="Tower", + title=None, + email="stower3@answers.com", + phone="151-539-6028", + address_line="93001 Arizona Drive", + city="Columbus", + state_territory="Oh", + zipcode="43268", + ) + expected_transition_domain.id = transition.id - self.assertEqual(transition, expected_transition_domain) + self.assertEqual(transition, expected_transition_domain) def test_transition_domain_status_unknown(self): """ Test that a domain in unknown status can be loaded - """ # noqa - E501 (harder to read) - # == First, parse all existing data == # - self.run_load_domains() - self.run_transfer_domains() + """ + with less_console_noise(): + # noqa - E501 (harder to read) + # == First, parse all existing data == # + self.run_load_domains() + self.run_transfer_domains() - domain_object = Domain.objects.get(name="fakewebsite3.gov") - self.assertEqual(domain_object.state, Domain.State.UNKNOWN) + domain_object = Domain.objects.get(name="fakewebsite3.gov") + self.assertEqual(domain_object.state, Domain.State.UNKNOWN) def test_load_organization_data_domain_information(self): """ @@ -367,35 +381,38 @@ class TestOrganizationMigration(TestCase): The test fetches the actual DomainInformation object from the database and compares it with the expected object. """ - # == First, parse all existing data == # - self.run_load_domains() - self.run_transfer_domains() + with less_console_noise(): + # == First, parse all existing data == # + self.run_load_domains() + self.run_transfer_domains() - # == Second, try adding org data to it == # - self.run_load_organization_data() + # == Second, try adding org data to it == # + self.run_load_organization_data() - # == Third, test that we've loaded data as we expect == # - _domain = Domain.objects.filter(name="fakewebsite2.gov").get() - domain_information = DomainInformation.objects.filter(domain=_domain).get() + # == Third, test that we've loaded data as we expect == # + _domain = Domain.objects.filter(name="fakewebsite2.gov").get() + domain_information = DomainInformation.objects.filter(domain=_domain).get() - expected_creator = User.objects.filter(username="System").get() - expected_ao = Contact.objects.filter(first_name="Seline", middle_name="testmiddle2", last_name="Tower").get() - expected_domain_information = DomainInformation( - creator=expected_creator, - organization_type="federal", - federal_agency="Department of Commerce", - federal_type="executive", - organization_name="Fanoodle", - address_line1="93001 Arizona Drive", - city="Columbus", - state_territory="Oh", - zipcode="43268", - authorizing_official=expected_ao, - domain=_domain, - ) - # Given that these are different objects, this needs to be set - expected_domain_information.id = domain_information.id - self.assertEqual(domain_information, expected_domain_information) + expected_creator = User.objects.filter(username="System").get() + expected_ao = Contact.objects.filter( + first_name="Seline", middle_name="testmiddle2", last_name="Tower" + ).get() + expected_domain_information = DomainInformation( + creator=expected_creator, + organization_type="federal", + federal_agency="Department of Commerce", + federal_type="executive", + organization_name="Fanoodle", + address_line1="93001 Arizona Drive", + city="Columbus", + state_territory="Oh", + zipcode="43268", + authorizing_official=expected_ao, + domain=_domain, + ) + # Given that these are different objects, this needs to be set + expected_domain_information.id = domain_information.id + self.assertEqual(domain_information, expected_domain_information) def test_load_organization_data_preserves_existing_data(self): """ @@ -410,44 +427,47 @@ class TestOrganizationMigration(TestCase): The expected result is that the DomainInformation object retains its pre-existing data after the load_organization_data method is run. """ - # == First, parse all existing data == # - self.run_load_domains() - self.run_transfer_domains() + with less_console_noise(): + # == First, parse all existing data == # + self.run_load_domains() + self.run_transfer_domains() - # == Second, try add prexisting fake data == # - _domain_old = Domain.objects.filter(name="fakewebsite2.gov").get() - domain_information_old = DomainInformation.objects.filter(domain=_domain_old).get() - domain_information_old.address_line1 = "93001 Galactic Way" - domain_information_old.city = "Olympus" - domain_information_old.state_territory = "MA" - domain_information_old.zipcode = "12345" - domain_information_old.save() + # == Second, try add prexisting fake data == # + _domain_old = Domain.objects.filter(name="fakewebsite2.gov").get() + domain_information_old = DomainInformation.objects.filter(domain=_domain_old).get() + domain_information_old.address_line1 = "93001 Galactic Way" + domain_information_old.city = "Olympus" + domain_information_old.state_territory = "MA" + domain_information_old.zipcode = "12345" + domain_information_old.save() - # == Third, try running the script == # - self.run_load_organization_data() + # == Third, try running the script == # + self.run_load_organization_data() - # == Fourth, test that no data is overwritten as we expect == # - _domain = Domain.objects.filter(name="fakewebsite2.gov").get() - domain_information = DomainInformation.objects.filter(domain=_domain).get() + # == Fourth, test that no data is overwritten as we expect == # + _domain = Domain.objects.filter(name="fakewebsite2.gov").get() + domain_information = DomainInformation.objects.filter(domain=_domain).get() - expected_creator = User.objects.filter(username="System").get() - expected_ao = Contact.objects.filter(first_name="Seline", middle_name="testmiddle2", last_name="Tower").get() - expected_domain_information = DomainInformation( - creator=expected_creator, - organization_type="federal", - federal_agency="Department of Commerce", - federal_type="executive", - organization_name="Fanoodle", - address_line1="93001 Galactic Way", - city="Olympus", - state_territory="MA", - zipcode="12345", - authorizing_official=expected_ao, - domain=_domain, - ) - # Given that these are different objects, this needs to be set - expected_domain_information.id = domain_information.id - self.assertEqual(domain_information, expected_domain_information) + expected_creator = User.objects.filter(username="System").get() + expected_ao = Contact.objects.filter( + first_name="Seline", middle_name="testmiddle2", last_name="Tower" + ).get() + expected_domain_information = DomainInformation( + creator=expected_creator, + organization_type="federal", + federal_agency="Department of Commerce", + federal_type="executive", + organization_name="Fanoodle", + address_line1="93001 Galactic Way", + city="Olympus", + state_territory="MA", + zipcode="12345", + authorizing_official=expected_ao, + domain=_domain, + ) + # Given that these are different objects, this needs to be set + expected_domain_information.id = domain_information.id + self.assertEqual(domain_information, expected_domain_information) def test_load_organization_data_integrity(self): """ @@ -462,29 +482,30 @@ class TestOrganizationMigration(TestCase): The expected result is that the counts of objects in the database match the expected counts, indicating that the data has not been corrupted. """ - # First, parse all existing data - self.run_load_domains() - self.run_transfer_domains() + with less_console_noise(): + # First, parse all existing data + self.run_load_domains() + self.run_transfer_domains() - # Second, try adding org data to it - self.run_load_organization_data() + # Second, try adding org data to it + self.run_load_organization_data() - # Third, test that we didn't corrupt any data - expected_total_transition_domains = 9 - expected_total_domains = 5 - expected_total_domain_informations = 5 + # Third, test that we didn't corrupt any data + expected_total_transition_domains = 9 + expected_total_domains = 5 + expected_total_domain_informations = 5 - expected_missing_domains = 0 - expected_duplicate_domains = 0 - expected_missing_domain_informations = 0 - self.compare_tables( - expected_total_transition_domains, - expected_total_domains, - expected_total_domain_informations, - expected_missing_domains, - expected_duplicate_domains, - expected_missing_domain_informations, - ) + expected_missing_domains = 0 + expected_duplicate_domains = 0 + expected_missing_domain_informations = 0 + self.compare_tables( + expected_total_transition_domains, + expected_total_domains, + expected_total_domain_informations, + expected_missing_domains, + expected_duplicate_domains, + expected_missing_domain_informations, + ) class TestMigrations(TestCase): @@ -521,39 +542,42 @@ class TestMigrations(TestCase): UserDomainRole.objects.all().delete() def run_load_domains(self): - # noqa here because splitting this up makes it confusing. - # ES501 - with patch( - "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa - return_value=True, - ): - call_command( - "load_transition_domain", - self.migration_json_filename, - directory=self.test_data_file_location, - ) - - def run_transfer_domains(self): - call_command("transfer_transition_domains_to_domains") - - def run_master_script(self): - # noqa here (E501) because splitting this up makes it - # confusing to read. - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + # noqa here because splitting this up makes it confusing. + # ES501 with patch( "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa return_value=True, ): - with patch("registrar.utility.email.send_templated_email", return_value=None): - call_command( - "master_domain_migrations", - runMigrations=True, - migrationDirectory=self.test_data_file_location, - migrationJSON=self.migration_json_filename, - disablePrompts=True, - ) - print(f"here: {mock_client.EMAILS_SENT}") + call_command( + "load_transition_domain", + self.migration_json_filename, + directory=self.test_data_file_location, + ) + + def run_transfer_domains(self): + with less_console_noise(): + call_command("transfer_transition_domains_to_domains") + + def run_master_script(self): + with less_console_noise(): + # noqa here (E501) because splitting this up makes it + # confusing to read. + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with patch( + "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa + return_value=True, + ): + with patch("registrar.utility.email.send_templated_email", return_value=None): + call_command( + "master_domain_migrations", + runMigrations=True, + migrationDirectory=self.test_data_file_location, + migrationJSON=self.migration_json_filename, + disablePrompts=True, + ) + logger.debug(f"here: {mock_client.EMAILS_SENT}") def compare_tables( self, @@ -607,7 +631,7 @@ class TestMigrations(TestCase): total_domain_informations = len(DomainInformation.objects.all()) total_domain_invitations = len(DomainInvitation.objects.all()) - print( + logger.debug( f""" total_missing_domains = {len(missing_domains)} total_duplicate_domains = {len(duplicate_domains)} @@ -636,225 +660,230 @@ class TestMigrations(TestCase): follow best practice of limiting the number of assertions per test. But for now, this will double-check that the script works as intended.""" + with less_console_noise(): + self.run_master_script() - self.run_master_script() + # STEP 2: (analyze the tables just like the + # migration script does, but add assert statements) + expected_total_transition_domains = 9 + expected_total_domains = 5 + expected_total_domain_informations = 5 + expected_total_domain_invitations = 8 - # STEP 2: (analyze the tables just like the - # migration script does, but add assert statements) - expected_total_transition_domains = 9 - expected_total_domains = 5 - expected_total_domain_informations = 5 - expected_total_domain_invitations = 8 - - expected_missing_domains = 0 - expected_duplicate_domains = 0 - expected_missing_domain_informations = 0 - # we expect 1 missing invite from anomaly.gov (an injected error) - expected_missing_domain_invitations = 1 - self.compare_tables( - expected_total_transition_domains, - expected_total_domains, - expected_total_domain_informations, - expected_total_domain_invitations, - expected_missing_domains, - expected_duplicate_domains, - expected_missing_domain_informations, - expected_missing_domain_invitations, - ) + expected_missing_domains = 0 + expected_duplicate_domains = 0 + expected_missing_domain_informations = 0 + # we expect 1 missing invite from anomaly.gov (an injected error) + expected_missing_domain_invitations = 1 + self.compare_tables( + expected_total_transition_domains, + expected_total_domains, + expected_total_domain_informations, + expected_total_domain_invitations, + expected_missing_domains, + expected_duplicate_domains, + expected_missing_domain_informations, + expected_missing_domain_invitations, + ) def test_load_empty_transition_domain(self): """Loads TransitionDomains without additional data""" - self.run_load_domains() + with less_console_noise(): + self.run_load_domains() - # STEP 2: (analyze the tables just like the migration - # script does, but add assert statements) - expected_total_transition_domains = 9 - expected_total_domains = 0 - expected_total_domain_informations = 0 - expected_total_domain_invitations = 0 + # STEP 2: (analyze the tables just like the migration + # script does, but add assert statements) + expected_total_transition_domains = 9 + expected_total_domains = 0 + expected_total_domain_informations = 0 + expected_total_domain_invitations = 0 - expected_missing_domains = 9 - expected_duplicate_domains = 0 - expected_missing_domain_informations = 9 - expected_missing_domain_invitations = 9 - self.compare_tables( - expected_total_transition_domains, - expected_total_domains, - expected_total_domain_informations, - expected_total_domain_invitations, - expected_missing_domains, - expected_duplicate_domains, - expected_missing_domain_informations, - expected_missing_domain_invitations, - ) + expected_missing_domains = 9 + expected_duplicate_domains = 0 + expected_missing_domain_informations = 9 + expected_missing_domain_invitations = 9 + self.compare_tables( + expected_total_transition_domains, + expected_total_domains, + expected_total_domain_informations, + expected_total_domain_invitations, + expected_missing_domains, + expected_duplicate_domains, + expected_missing_domain_informations, + expected_missing_domain_invitations, + ) def test_load_full_domain(self): - self.run_load_domains() - self.run_transfer_domains() + with less_console_noise(): + self.run_load_domains() + self.run_transfer_domains() - # Analyze the tables - expected_total_transition_domains = 9 - expected_total_domains = 5 - expected_total_domain_informations = 5 - expected_total_domain_invitations = 8 + # Analyze the tables + expected_total_transition_domains = 9 + expected_total_domains = 5 + expected_total_domain_informations = 5 + expected_total_domain_invitations = 8 - expected_missing_domains = 0 - expected_duplicate_domains = 0 - expected_missing_domain_informations = 0 - expected_missing_domain_invitations = 1 - self.compare_tables( - expected_total_transition_domains, - expected_total_domains, - expected_total_domain_informations, - expected_total_domain_invitations, - expected_missing_domains, - expected_duplicate_domains, - expected_missing_domain_informations, - expected_missing_domain_invitations, - ) + expected_missing_domains = 0 + expected_duplicate_domains = 0 + expected_missing_domain_informations = 0 + expected_missing_domain_invitations = 1 + self.compare_tables( + expected_total_transition_domains, + expected_total_domains, + expected_total_domain_informations, + expected_total_domain_invitations, + expected_missing_domains, + expected_duplicate_domains, + expected_missing_domain_informations, + expected_missing_domain_invitations, + ) - # Test created domains - anomaly_domains = Domain.objects.filter(name="anomaly.gov") - self.assertEqual(anomaly_domains.count(), 1) - anomaly = anomaly_domains.get() + # Test created domains + anomaly_domains = Domain.objects.filter(name="anomaly.gov") + self.assertEqual(anomaly_domains.count(), 1) + anomaly = anomaly_domains.get() - self.assertEqual(anomaly.expiration_date, datetime.date(2023, 3, 9)) + self.assertEqual(anomaly.expiration_date, datetime.date(2023, 3, 9)) - self.assertEqual(anomaly.name, "anomaly.gov") - self.assertEqual(anomaly.state, "ready") + self.assertEqual(anomaly.name, "anomaly.gov") + self.assertEqual(anomaly.state, "ready") - testdomain_domains = Domain.objects.filter(name="fakewebsite2.gov") - self.assertEqual(testdomain_domains.count(), 1) + testdomain_domains = Domain.objects.filter(name="fakewebsite2.gov") + self.assertEqual(testdomain_domains.count(), 1) - testdomain = testdomain_domains.get() + testdomain = testdomain_domains.get() - self.assertEqual(testdomain.expiration_date, datetime.date(2023, 9, 30)) - self.assertEqual(testdomain.name, "fakewebsite2.gov") - self.assertEqual(testdomain.state, "on hold") + self.assertEqual(testdomain.expiration_date, datetime.date(2023, 9, 30)) + self.assertEqual(testdomain.name, "fakewebsite2.gov") + self.assertEqual(testdomain.state, "on hold") def test_load_full_domain_information(self): - self.run_load_domains() - self.run_transfer_domains() + with less_console_noise(): + self.run_load_domains() + self.run_transfer_domains() - # Analyze the tables - expected_total_transition_domains = 9 - expected_total_domains = 5 - expected_total_domain_informations = 5 - expected_total_domain_invitations = 8 + # Analyze the tables + expected_total_transition_domains = 9 + expected_total_domains = 5 + expected_total_domain_informations = 5 + expected_total_domain_invitations = 8 - expected_missing_domains = 0 - expected_duplicate_domains = 0 - expected_missing_domain_informations = 0 - expected_missing_domain_invitations = 1 - self.compare_tables( - expected_total_transition_domains, - expected_total_domains, - expected_total_domain_informations, - expected_total_domain_invitations, - expected_missing_domains, - expected_duplicate_domains, - expected_missing_domain_informations, - expected_missing_domain_invitations, - ) + expected_missing_domains = 0 + expected_duplicate_domains = 0 + expected_missing_domain_informations = 0 + expected_missing_domain_invitations = 1 + self.compare_tables( + expected_total_transition_domains, + expected_total_domains, + expected_total_domain_informations, + expected_total_domain_invitations, + expected_missing_domains, + expected_duplicate_domains, + expected_missing_domain_informations, + expected_missing_domain_invitations, + ) - # Test created Domain Information objects - domain = Domain.objects.filter(name="anomaly.gov").get() - anomaly_domain_infos = DomainInformation.objects.filter(domain=domain) + # Test created Domain Information objects + domain = Domain.objects.filter(name="anomaly.gov").get() + anomaly_domain_infos = DomainInformation.objects.filter(domain=domain) - self.assertEqual(anomaly_domain_infos.count(), 1) + self.assertEqual(anomaly_domain_infos.count(), 1) - # This domain should be pretty barebones. Something isnt - # parsing right if we get a lot of data. - anomaly = anomaly_domain_infos.get() - self.assertEqual(anomaly.organization_name, "Flashdog") - self.assertEqual(anomaly.organization_type, None) - self.assertEqual(anomaly.federal_agency, None) - self.assertEqual(anomaly.federal_type, None) + # This domain should be pretty barebones. Something isnt + # parsing right if we get a lot of data. + anomaly = anomaly_domain_infos.get() + self.assertEqual(anomaly.organization_name, "Flashdog") + self.assertEqual(anomaly.organization_type, None) + self.assertEqual(anomaly.federal_agency, None) + self.assertEqual(anomaly.federal_type, None) - # Check for the "system" creator user - Users = User.objects.filter(username="System") - self.assertEqual(Users.count(), 1) - self.assertEqual(anomaly.creator, Users.get()) + # Check for the "system" creator user + Users = User.objects.filter(username="System") + self.assertEqual(Users.count(), 1) + self.assertEqual(anomaly.creator, Users.get()) - domain = Domain.objects.filter(name="fakewebsite2.gov").get() - fakewebsite_domain_infos = DomainInformation.objects.filter(domain=domain) - self.assertEqual(fakewebsite_domain_infos.count(), 1) + domain = Domain.objects.filter(name="fakewebsite2.gov").get() + fakewebsite_domain_infos = DomainInformation.objects.filter(domain=domain) + self.assertEqual(fakewebsite_domain_infos.count(), 1) - fakewebsite = fakewebsite_domain_infos.get() - self.assertEqual(fakewebsite.organization_name, "Fanoodle") - self.assertEqual(fakewebsite.organization_type, "federal") - self.assertEqual(fakewebsite.federal_agency, "Department of Commerce") - self.assertEqual(fakewebsite.federal_type, "executive") + fakewebsite = fakewebsite_domain_infos.get() + self.assertEqual(fakewebsite.organization_name, "Fanoodle") + self.assertEqual(fakewebsite.organization_type, "federal") + self.assertEqual(fakewebsite.federal_agency, "Department of Commerce") + self.assertEqual(fakewebsite.federal_type, "executive") - ao = fakewebsite.authorizing_official + ao = fakewebsite.authorizing_official - self.assertEqual(ao.first_name, "Seline") - self.assertEqual(ao.middle_name, "testmiddle2") - self.assertEqual(ao.last_name, "Tower") - self.assertEqual(ao.email, "stower3@answers.com") - self.assertEqual(ao.phone, "151-539-6028") + self.assertEqual(ao.first_name, "Seline") + self.assertEqual(ao.middle_name, "testmiddle2") + self.assertEqual(ao.last_name, "Tower") + self.assertEqual(ao.email, "stower3@answers.com") + self.assertEqual(ao.phone, "151-539-6028") - # Check for the "system" creator user - Users = User.objects.filter(username="System") - self.assertEqual(Users.count(), 1) - self.assertEqual(anomaly.creator, Users.get()) + # Check for the "system" creator user + Users = User.objects.filter(username="System") + self.assertEqual(Users.count(), 1) + self.assertEqual(anomaly.creator, Users.get()) def test_transfer_transition_domains_to_domains(self): - self.run_load_domains() - self.run_transfer_domains() + with less_console_noise(): + self.run_load_domains() + self.run_transfer_domains() - # Analyze the tables - expected_total_transition_domains = 9 - expected_total_domains = 5 - expected_total_domain_informations = 5 - expected_total_domain_invitations = 8 + # Analyze the tables + expected_total_transition_domains = 9 + expected_total_domains = 5 + expected_total_domain_informations = 5 + expected_total_domain_invitations = 8 - expected_missing_domains = 0 - expected_duplicate_domains = 0 - expected_missing_domain_informations = 0 - expected_missing_domain_invitations = 1 - self.compare_tables( - expected_total_transition_domains, - expected_total_domains, - expected_total_domain_informations, - expected_total_domain_invitations, - expected_missing_domains, - expected_duplicate_domains, - expected_missing_domain_informations, - expected_missing_domain_invitations, - ) + expected_missing_domains = 0 + expected_duplicate_domains = 0 + expected_missing_domain_informations = 0 + expected_missing_domain_invitations = 1 + self.compare_tables( + expected_total_transition_domains, + expected_total_domains, + expected_total_domain_informations, + expected_total_domain_invitations, + expected_missing_domains, + expected_duplicate_domains, + expected_missing_domain_informations, + expected_missing_domain_invitations, + ) def test_logins(self): - # TODO: setup manually instead of calling other scripts - self.run_load_domains() - self.run_transfer_domains() + with less_console_noise(): + # TODO: setup manually instead of calling other scripts + self.run_load_domains() + self.run_transfer_domains() - # Simluate Logins - for invite in DomainInvitation.objects.all(): - # get a user with this email address - user, user_created = User.objects.get_or_create(email=invite.email, username=invite.email) - user.on_each_login() + # Simluate Logins + for invite in DomainInvitation.objects.all(): + # get a user with this email address + user, user_created = User.objects.get_or_create(email=invite.email, username=invite.email) + user.on_each_login() - # Analyze the tables - expected_total_transition_domains = 9 - expected_total_domains = 5 - expected_total_domain_informations = 5 - expected_total_domain_invitations = 8 + # Analyze the tables + expected_total_transition_domains = 9 + expected_total_domains = 5 + expected_total_domain_informations = 5 + expected_total_domain_invitations = 8 - expected_missing_domains = 0 - expected_duplicate_domains = 0 - expected_missing_domain_informations = 0 - expected_missing_domain_invitations = 1 - self.compare_tables( - expected_total_transition_domains, - expected_total_domains, - expected_total_domain_informations, - expected_total_domain_invitations, - expected_missing_domains, - expected_duplicate_domains, - expected_missing_domain_informations, - expected_missing_domain_invitations, - ) + expected_missing_domains = 0 + expected_duplicate_domains = 0 + expected_missing_domain_informations = 0 + expected_missing_domain_invitations = 1 + self.compare_tables( + expected_total_transition_domains, + expected_total_domains, + expected_total_domain_informations, + expected_total_domain_invitations, + expected_missing_domains, + expected_duplicate_domains, + expected_missing_domain_informations, + expected_missing_domain_invitations, + ) @boto3_mocking.patching def test_send_domain_invitations_email(self): diff --git a/src/registrar/tests/test_url_auth.py b/src/registrar/tests/test_url_auth.py index 34f80ac44..3e0514a85 100644 --- a/src/registrar/tests/test_url_auth.py +++ b/src/registrar/tests/test_url_auth.py @@ -23,6 +23,7 @@ SAMPLE_KWARGS = { "content_type_id": "2", "object_id": "3", "domain": "whitehouse.gov", + "user_pk": "1", } # Our test suite will ignore some namespaces. diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index b1b112cb8..450993e5c 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1,44 +1,17 @@ -from unittest import skip -from unittest.mock import MagicMock, ANY, patch - -from django.conf import settings from django.test import Client, TestCase from django.urls import reverse from django.contrib.auth import get_user_model -from .common import MockEppLib, MockSESClient, completed_application, create_user # type: ignore -from django_webtest import WebTest # type: ignore -import boto3_mocking # type: ignore +from .common import MockEppLib # type: ignore -from registrar.utility.errors import ( - NameserverError, - NameserverErrorCodes, - SecurityEmailError, - SecurityEmailErrorCodes, - GenericError, - GenericErrorCodes, - DsDataError, - DsDataErrorCodes, -) from registrar.models import ( DomainApplication, - Domain, DomainInformation, DraftDomain, - DomainInvitation, Contact, - PublicContact, - Host, - HostIP, - Website, - UserDomainRole, User, ) -from registrar.views.application import ApplicationWizard, Step -from datetime import date, datetime, timedelta -from django.utils import timezone - from .common import less_console_noise import logging @@ -159,32 +132,33 @@ class LoggedInTests(TestWithUser): # Given that we are including a subset of items that can be deleted while excluding the rest, # subTest is appropriate here as otherwise we would need many duplicate tests for the same reason. - draft_domain = DraftDomain.objects.create(name="igorville.gov") - for status in DomainApplication.ApplicationStatus: - if status not in [ - DomainApplication.ApplicationStatus.STARTED, - DomainApplication.ApplicationStatus.WITHDRAWN, - ]: - with self.subTest(status=status): - application = DomainApplication.objects.create( - creator=self.user, requested_domain=draft_domain, status=status - ) + with less_console_noise(): + draft_domain = DraftDomain.objects.create(name="igorville.gov") + for status in DomainApplication.ApplicationStatus: + if status not in [ + DomainApplication.ApplicationStatus.STARTED, + DomainApplication.ApplicationStatus.WITHDRAWN, + ]: + with self.subTest(status=status): + application = DomainApplication.objects.create( + creator=self.user, requested_domain=draft_domain, status=status + ) - # Trigger the delete logic - response = self.client.post( - reverse("application-delete", kwargs={"pk": application.pk}), follow=True - ) + # Trigger the delete logic + response = self.client.post( + reverse("application-delete", kwargs={"pk": application.pk}), follow=True + ) - # Check for a 403 error - the end user should not be allowed to do this - self.assertEqual(response.status_code, 403) + # Check for a 403 error - the end user should not be allowed to do this + self.assertEqual(response.status_code, 403) - desired_application = DomainApplication.objects.filter(requested_domain=draft_domain) + desired_application = DomainApplication.objects.filter(requested_domain=draft_domain) - # Make sure the DomainApplication wasn't deleted - self.assertEqual(desired_application.count(), 1) + # Make sure the DomainApplication wasn't deleted + self.assertEqual(desired_application.count(), 1) - # clean up - application.delete() + # clean up + application.delete() def test_home_deletes_domain_application_and_orphans(self): """Tests if delete for DomainApplication deletes orphaned Contact objects""" @@ -329,3581 +303,4 @@ class LoggedInTests(TestWithUser): with less_console_noise(): response = self.client.get("/request/", follow=True) - print(response.status_code) self.assertEqual(response.status_code, 403) - - -class DomainApplicationTests(TestWithUser, WebTest): - - """Webtests for domain application to test filling and submitting.""" - - # Doesn't work with CSRF checking - # hypothesis is that CSRF_USE_SESSIONS is incompatible with WebTest - csrf_checks = False - - def setUp(self): - super().setUp() - self.app.set_user(self.user.username) - self.TITLES = ApplicationWizard.TITLES - - def test_application_form_intro_acknowledgement(self): - """Tests that user is presented with intro acknowledgement page""" - intro_page = self.app.get(reverse("application:")) - self.assertContains(intro_page, "You’re about to start your .gov domain request") - - def test_application_form_intro_is_skipped_when_edit_access(self): - """Tests that user is NOT presented with intro acknowledgement page when accessed through 'edit'""" - completed_application(status=DomainApplication.ApplicationStatus.STARTED, user=self.user) - home_page = self.app.get("/") - self.assertContains(home_page, "city.gov") - # click the "Edit" link - detail_page = home_page.click("Edit", index=0) - # Check that the response is a redirect - self.assertEqual(detail_page.status_code, 302) - # You can access the 'Location' header to get the redirect URL - redirect_url = detail_page.url - self.assertEqual(redirect_url, "/request/organization_type/") - - def test_application_form_empty_submit(self): - """Tests empty submit on the first page after the acknowledgement page""" - intro_page = self.app.get(reverse("application:")) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - # submitting should get back the same page if the required field is empty - result = type_page.forms[0].submit() - self.assertIn("What kind of U.S.-based government organization do you represent?", result) - - def test_application_multiple_applications_exist(self): - """Test that an info message appears when user has multiple applications already""" - # create and submit an application - application = completed_application(user=self.user) - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - application.submit() - application.save() - - # now, attempt to create another one - with less_console_noise(): - intro_page = self.app.get(reverse("application:")) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - self.assertContains(type_page, "You cannot submit this request yet") - - @boto3_mocking.patching - def test_application_form_submission(self): - """ - Can fill out the entire form and submit. - As we add additional form pages, we need to include them here to make - this test work. - - This test also looks for the long organization name on the summary page. - - This also tests for the presence of a modal trigger and the dynamic test - in the modal header on the submit page. - """ - num_pages_tested = 0 - # elections, type_of_work, tribal_government - SKIPPED_PAGES = 3 - num_pages = len(self.TITLES) - SKIPPED_PAGES - - intro_page = self.app.get(reverse("application:")) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - # ---- TYPE PAGE ---- - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "federal" - # test next button and validate data - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - # should see results in db - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.organization_type, "federal") - # the post request should return a redirect to the next form in - # the application - self.assertEqual(type_result.status_code, 302) - self.assertEqual(type_result["Location"], "/request/organization_federal/") - num_pages_tested += 1 - - # ---- FEDERAL BRANCH PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - federal_page = type_result.follow() - federal_form = federal_page.forms[0] - federal_form["organization_federal-federal_type"] = "executive" - - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - federal_result = federal_form.submit() - # validate that data from this step are being saved - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.federal_type, "executive") - # the post request should return a redirect to the next form in - # the application - self.assertEqual(federal_result.status_code, 302) - self.assertEqual(federal_result["Location"], "/request/organization_contact/") - num_pages_tested += 1 - - # ---- ORG CONTACT PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - org_contact_page = federal_result.follow() - org_contact_form = org_contact_page.forms[0] - # federal agency so we have to fill in federal_agency - org_contact_form["organization_contact-federal_agency"] = "General Services Administration" - org_contact_form["organization_contact-organization_name"] = "Testorg" - org_contact_form["organization_contact-address_line1"] = "address 1" - org_contact_form["organization_contact-address_line2"] = "address 2" - org_contact_form["organization_contact-city"] = "NYC" - org_contact_form["organization_contact-state_territory"] = "NY" - org_contact_form["organization_contact-zipcode"] = "10002" - org_contact_form["organization_contact-urbanization"] = "URB Royal Oaks" - - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - org_contact_result = org_contact_form.submit() - # validate that data from this step are being saved - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.organization_name, "Testorg") - self.assertEqual(application.address_line1, "address 1") - self.assertEqual(application.address_line2, "address 2") - self.assertEqual(application.city, "NYC") - self.assertEqual(application.state_territory, "NY") - self.assertEqual(application.zipcode, "10002") - self.assertEqual(application.urbanization, "URB Royal Oaks") - # the post request should return a redirect to the next form in - # the application - self.assertEqual(org_contact_result.status_code, 302) - self.assertEqual(org_contact_result["Location"], "/request/authorizing_official/") - num_pages_tested += 1 - - # ---- AUTHORIZING OFFICIAL PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_page = org_contact_result.follow() - ao_form = ao_page.forms[0] - ao_form["authorizing_official-first_name"] = "Testy ATO" - ao_form["authorizing_official-last_name"] = "Tester ATO" - ao_form["authorizing_official-title"] = "Chief Tester" - ao_form["authorizing_official-email"] = "testy@town.com" - - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_result = ao_form.submit() - # validate that data from this step are being saved - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.authorizing_official.first_name, "Testy ATO") - self.assertEqual(application.authorizing_official.last_name, "Tester ATO") - self.assertEqual(application.authorizing_official.title, "Chief Tester") - self.assertEqual(application.authorizing_official.email, "testy@town.com") - # the post request should return a redirect to the next form in - # the application - self.assertEqual(ao_result.status_code, 302) - self.assertEqual(ao_result["Location"], "/request/current_sites/") - num_pages_tested += 1 - - # ---- CURRENT SITES PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - current_sites_page = ao_result.follow() - current_sites_form = current_sites_page.forms[0] - current_sites_form["current_sites-0-website"] = "www.city.com" - - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - current_sites_result = current_sites_form.submit() - # validate that data from this step are being saved - application = DomainApplication.objects.get() # there's only one - self.assertEqual( - application.current_websites.filter(website="http://www.city.com").count(), - 1, - ) - # the post request should return a redirect to the next form in - # the application - self.assertEqual(current_sites_result.status_code, 302) - self.assertEqual(current_sites_result["Location"], "/request/dotgov_domain/") - num_pages_tested += 1 - - # ---- DOTGOV DOMAIN PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - dotgov_page = current_sites_result.follow() - dotgov_form = dotgov_page.forms[0] - dotgov_form["dotgov_domain-requested_domain"] = "city" - dotgov_form["dotgov_domain-0-alternative_domain"] = "city1" - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - dotgov_result = dotgov_form.submit() - # validate that data from this step are being saved - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.requested_domain.name, "city.gov") - self.assertEqual(application.alternative_domains.filter(website="city1.gov").count(), 1) - # the post request should return a redirect to the next form in - # the application - self.assertEqual(dotgov_result.status_code, 302) - self.assertEqual(dotgov_result["Location"], "/request/purpose/") - num_pages_tested += 1 - - # ---- PURPOSE PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - purpose_page = dotgov_result.follow() - purpose_form = purpose_page.forms[0] - purpose_form["purpose-purpose"] = "For all kinds of things." - - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - purpose_result = purpose_form.submit() - # validate that data from this step are being saved - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.purpose, "For all kinds of things.") - # the post request should return a redirect to the next form in - # the application - self.assertEqual(purpose_result.status_code, 302) - self.assertEqual(purpose_result["Location"], "/request/your_contact/") - num_pages_tested += 1 - - # ---- YOUR CONTACT INFO PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - your_contact_page = purpose_result.follow() - your_contact_form = your_contact_page.forms[0] - - your_contact_form["your_contact-first_name"] = "Testy you" - your_contact_form["your_contact-last_name"] = "Tester you" - your_contact_form["your_contact-title"] = "Admin Tester" - your_contact_form["your_contact-email"] = "testy-admin@town.com" - your_contact_form["your_contact-phone"] = "(201) 555 5556" - - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - your_contact_result = your_contact_form.submit() - # validate that data from this step are being saved - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.submitter.first_name, "Testy you") - self.assertEqual(application.submitter.last_name, "Tester you") - self.assertEqual(application.submitter.title, "Admin Tester") - self.assertEqual(application.submitter.email, "testy-admin@town.com") - self.assertEqual(application.submitter.phone, "(201) 555 5556") - # the post request should return a redirect to the next form in - # the application - self.assertEqual(your_contact_result.status_code, 302) - self.assertEqual(your_contact_result["Location"], "/request/other_contacts/") - num_pages_tested += 1 - - # ---- OTHER CONTACTS PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - other_contacts_page = your_contact_result.follow() - - # This page has 3 forms in 1. - # Let's set the yes/no radios to enable the other contacts fieldsets - other_contacts_form = other_contacts_page.forms[0] - - other_contacts_form["other_contacts-has_other_contacts"] = "True" - - other_contacts_form["other_contacts-0-first_name"] = "Testy2" - other_contacts_form["other_contacts-0-last_name"] = "Tester2" - other_contacts_form["other_contacts-0-title"] = "Another Tester" - other_contacts_form["other_contacts-0-email"] = "testy2@town.com" - other_contacts_form["other_contacts-0-phone"] = "(201) 555 5557" - - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - other_contacts_result = other_contacts_form.submit() - # validate that data from this step are being saved - application = DomainApplication.objects.get() # there's only one - self.assertEqual( - application.other_contacts.filter( - first_name="Testy2", - last_name="Tester2", - title="Another Tester", - email="testy2@town.com", - phone="(201) 555 5557", - ).count(), - 1, - ) - # the post request should return a redirect to the next form in - # the application - self.assertEqual(other_contacts_result.status_code, 302) - self.assertEqual(other_contacts_result["Location"], "/request/anything_else/") - num_pages_tested += 1 - - # ---- ANYTHING ELSE PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - anything_else_page = other_contacts_result.follow() - anything_else_form = anything_else_page.forms[0] - - anything_else_form["anything_else-anything_else"] = "Nothing else." - - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - anything_else_result = anything_else_form.submit() - # validate that data from this step are being saved - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.anything_else, "Nothing else.") - # the post request should return a redirect to the next form in - # the application - self.assertEqual(anything_else_result.status_code, 302) - self.assertEqual(anything_else_result["Location"], "/request/requirements/") - num_pages_tested += 1 - - # ---- REQUIREMENTS PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - requirements_page = anything_else_result.follow() - requirements_form = requirements_page.forms[0] - - requirements_form["requirements-is_policy_acknowledged"] = True - - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - requirements_result = requirements_form.submit() - # validate that data from this step are being saved - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.is_policy_acknowledged, True) - # the post request should return a redirect to the next form in - # the application - self.assertEqual(requirements_result.status_code, 302) - self.assertEqual(requirements_result["Location"], "/request/review/") - num_pages_tested += 1 - - # ---- REVIEW AND FINSIHED PAGES ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - review_page = requirements_result.follow() - review_form = review_page.forms[0] - - # Review page contains all the previously entered data - # Let's make sure the long org name is displayed - self.assertContains(review_page, "Federal: an agency of the U.S. government") - self.assertContains(review_page, "Executive") - self.assertContains(review_page, "Testorg") - self.assertContains(review_page, "address 1") - self.assertContains(review_page, "address 2") - self.assertContains(review_page, "NYC") - self.assertContains(review_page, "NY") - self.assertContains(review_page, "10002") - self.assertContains(review_page, "URB Royal Oaks") - self.assertContains(review_page, "Testy ATO") - self.assertContains(review_page, "Tester ATO") - self.assertContains(review_page, "Chief Tester") - self.assertContains(review_page, "testy@town.com") - self.assertContains(review_page, "city.com") - self.assertContains(review_page, "city.gov") - self.assertContains(review_page, "city1.gov") - self.assertContains(review_page, "For all kinds of things.") - self.assertContains(review_page, "Testy you") - self.assertContains(review_page, "Tester you") - self.assertContains(review_page, "Admin Tester") - self.assertContains(review_page, "testy-admin@town.com") - self.assertContains(review_page, "(201) 555-5556") - self.assertContains(review_page, "Testy2") - self.assertContains(review_page, "Tester2") - self.assertContains(review_page, "Another Tester") - self.assertContains(review_page, "testy2@town.com") - self.assertContains(review_page, "(201) 555-5557") - self.assertContains(review_page, "Nothing else.") - - # We can't test the modal itself as it relies on JS for init and triggering, - # but we can test for the existence of its trigger: - self.assertContains(review_page, "toggle-submit-domain-request") - # And the existence of the modal's data parked and ready for the js init. - # The next assert also tests for the passed requested domain context from - # the view > application_form > modal - self.assertContains(review_page, "You are about to submit a domain request for city.gov") - - # final submission results in a redirect to the "finished" URL - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - with less_console_noise(): - review_result = review_form.submit() - - self.assertEqual(review_result.status_code, 302) - self.assertEqual(review_result["Location"], "/request/finished/") - num_pages_tested += 1 - - # following this redirect is a GET request, so include the cookie - # here too. - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - with less_console_noise(): - final_result = review_result.follow() - self.assertContains(final_result, "Thanks for your domain request!") - - # check that any new pages are added to this test - self.assertEqual(num_pages, num_pages_tested) - - # This is the start of a test to check an existing application, it currently - # does not work and results in errors as noted in: - # https://github.com/cisagov/getgov/pull/728 - @skip("WIP") - def test_application_form_started_allsteps(self): - num_pages_tested = 0 - # elections, type_of_work, tribal_government - SKIPPED_PAGES = 3 - DASHBOARD_PAGE = 1 - num_pages = len(self.TITLES) - SKIPPED_PAGES + DASHBOARD_PAGE - - application = completed_application(user=self.user) - application.save() - home_page = self.app.get("/") - self.assertContains(home_page, "city.gov") - self.assertContains(home_page, "Started") - num_pages_tested += 1 - - # TODO: For some reason this click results in a new application being generated - # This appraoch is an alternatie to using get as is being done below - # - # type_page = home_page.click("Edit") - - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - url = reverse("edit-application", kwargs={"id": application.pk}) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - # TODO: The following line results in a django error on middleware - response = self.client.get(url, follow=True) - self.assertContains(response, "Type of organization") - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # TODO: Step through the remaining pages - - self.assertEqual(num_pages, num_pages_tested) - - def test_application_form_conditional_federal(self): - """Federal branch question is shown for federal organizations.""" - intro_page = self.app.get(reverse("application:")) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - # ---- TYPE PAGE ---- - - # the conditional step titles shouldn't appear initially - self.assertNotContains(type_page, self.TITLES["organization_federal"]) - self.assertNotContains(type_page, self.TITLES["organization_election"]) - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "federal" - - # set the session ID before .submit() - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - - # the post request should return a redirect to the federal branch - # question - self.assertEqual(type_result.status_code, 302) - self.assertEqual(type_result["Location"], "/request/organization_federal/") - - # and the step label should appear in the sidebar of the resulting page - # but the step label for the elections page should not appear - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - federal_page = type_result.follow() - self.assertContains(federal_page, self.TITLES["organization_federal"]) - self.assertNotContains(federal_page, self.TITLES["organization_election"]) - - # continuing on in the flow we need to see top-level agency on the - # contact page - federal_page.forms[0]["organization_federal-federal_type"] = "executive" - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - federal_result = federal_page.forms[0].submit() - # the post request should return a redirect to the contact - # question - self.assertEqual(federal_result.status_code, 302) - self.assertEqual(federal_result["Location"], "/request/organization_contact/") - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - contact_page = federal_result.follow() - self.assertContains(contact_page, "Federal agency") - - def test_application_form_conditional_elections(self): - """Election question is shown for other organizations.""" - intro_page = self.app.get(reverse("application:")) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - # ---- TYPE PAGE ---- - - # the conditional step titles shouldn't appear initially - self.assertNotContains(type_page, self.TITLES["organization_federal"]) - self.assertNotContains(type_page, self.TITLES["organization_election"]) - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "county" - - # set the session ID before .submit() - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - - # the post request should return a redirect to the elections question - self.assertEqual(type_result.status_code, 302) - self.assertEqual(type_result["Location"], "/request/organization_election/") - - # and the step label should appear in the sidebar of the resulting page - # but the step label for the elections page should not appear - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - election_page = type_result.follow() - self.assertContains(election_page, self.TITLES["organization_election"]) - self.assertNotContains(election_page, self.TITLES["organization_federal"]) - - # continuing on in the flow we need to NOT see top-level agency on the - # contact page - election_page.forms[0]["organization_election-is_election_board"] = "True" - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - election_result = election_page.forms[0].submit() - # the post request should return a redirect to the contact - # question - self.assertEqual(election_result.status_code, 302) - self.assertEqual(election_result["Location"], "/request/organization_contact/") - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - contact_page = election_result.follow() - self.assertNotContains(contact_page, "Federal agency") - - def test_application_form_section_skipping(self): - """Can skip forward and back in sections""" - intro_page = self.app.get(reverse("application:")) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "federal" - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - federal_page = type_result.follow() - - # Now on federal type page, click back to the organization type - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - new_page = federal_page.click(str(self.TITLES["organization_type"]), index=0) - - # Should be a link to the organization_federal page - self.assertGreater( - len(new_page.html.find_all("a", href="/request/organization_federal/")), - 0, - ) - - def test_application_form_nonfederal(self): - """Non-federal organizations don't have to provide their federal agency.""" - intro_page = self.app.get(reverse("application:")) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.INTERSTATE - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - contact_page = type_result.follow() - org_contact_form = contact_page.forms[0] - - self.assertNotIn("federal_agency", org_contact_form.fields) - - # minimal fields that must be filled out - org_contact_form["organization_contact-organization_name"] = "Testorg" - org_contact_form["organization_contact-address_line1"] = "address 1" - org_contact_form["organization_contact-city"] = "NYC" - org_contact_form["organization_contact-state_territory"] = "NY" - org_contact_form["organization_contact-zipcode"] = "10002" - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - contact_result = org_contact_form.submit() - - # the post request should return a redirect to the - # about your organization page if it was successful. - self.assertEqual(contact_result.status_code, 302) - self.assertEqual(contact_result["Location"], "/request/about_your_organization/") - - def test_application_about_your_organization_special(self): - """Special districts have to answer an additional question.""" - intro_page = self.app.get(reverse("application:")) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.SPECIAL_DISTRICT - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_page.forms[0].submit() - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - contact_page = type_result.follow() - - self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION]) - - def test_yes_no_form_inits_blank_for_new_application(self): - """On the Other Contacts page, the yes/no form gets initialized with nothing selected for - new applications""" - other_contacts_page = self.app.get(reverse("application:other_contacts")) - other_contacts_form = other_contacts_page.forms[0] - self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None) - - def test_yes_no_form_inits_yes_for_application_with_other_contacts(self): - """On the Other Contacts page, the yes/no form gets initialized with YES selected if the - application has other contacts""" - # Application has other contacts by default - application = completed_application(user=self.user) - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") - - def test_yes_no_form_inits_no_for_application_with_no_other_contacts_rationale(self): - """On the Other Contacts page, the yes/no form gets initialized with NO selected if the - application has no other contacts""" - # Application has other contacts by default - application = completed_application(user=self.user, has_other_contacts=False) - application.no_other_contacts_rationale = "Hello!" - application.save() - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False") - - def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self): - """When a user submits the Other Contacts form with other contacts selected, the application's - no other contacts rationale gets deleted""" - # Application has other contacts by default - application = completed_application(user=self.user, has_other_contacts=False) - application.no_other_contacts_rationale = "Hello!" - application.save() - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False") - - other_contacts_form["other_contacts-has_other_contacts"] = "True" - - other_contacts_form["other_contacts-0-first_name"] = "Testy" - other_contacts_form["other_contacts-0-middle_name"] = "" - other_contacts_form["other_contacts-0-last_name"] = "McTesterson" - other_contacts_form["other_contacts-0-title"] = "Lord" - other_contacts_form["other_contacts-0-email"] = "testy@abc.org" - other_contacts_form["other_contacts-0-phone"] = "(201) 555-0123" - - # Submit the now empty form - other_contacts_form.submit() - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - # Verify that the no_other_contacts_rationale we saved earlier has been removed from the database - application = DomainApplication.objects.get() - self.assertEqual( - application.other_contacts.count(), - 1, - ) - - self.assertEquals( - application.no_other_contacts_rationale, - None, - ) - - def test_submitting_no_other_contacts_rationale_deletes_other_contacts(self): - """When a user submits the Other Contacts form with no other contacts selected, the application's - other contacts get deleted for other contacts that exist and are not joined to other objects - """ - # Application has other contacts by default - application = completed_application(user=self.user) - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") - - other_contacts_form["other_contacts-has_other_contacts"] = "False" - - other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!" - - # Submit the now empty form - other_contacts_form.submit() - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - # Verify that the no_other_contacts_rationale we saved earlier has been removed from the database - application = DomainApplication.objects.get() - self.assertEqual( - application.other_contacts.count(), - 0, - ) - - self.assertEquals( - application.no_other_contacts_rationale, - "Hello again!", - ) - - def test_submitting_no_other_contacts_rationale_removes_reference_other_contacts_when_joined(self): - """When a user submits the Other Contacts form with no other contacts selected, the application's - other contacts references get removed for other contacts that exist and are joined to other objects""" - # Populate the database with a domain application that - # has 1 "other contact" assigned to it - # We'll do it from scratch so we can reuse the other contact - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(555) 555 5555", - ) - you, _ = Contact.objects.get_or_create( - first_name="Testy you", - last_name="Tester you", - title="Admin Tester", - email="testy-admin@town.com", - phone="(555) 555 5556", - ) - other, _ = Contact.objects.get_or_create( - first_name="Testy2", - last_name="Tester2", - title="Another Tester", - email="testy2@town.com", - phone="(555) 555 5557", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - submitter=you, - creator=self.user, - status="started", - ) - application.other_contacts.add(other) - - # Now let's join the other contact to another object - domain_info = DomainInformation.objects.create(creator=self.user) - domain_info.other_contacts.set([other]) - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") - - other_contacts_form["other_contacts-has_other_contacts"] = "False" - - other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!" - - # Submit the now empty form - other_contacts_form.submit() - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - # Verify that the no_other_contacts_rationale we saved earlier is no longer associated with the application - application = DomainApplication.objects.get() - self.assertEqual( - application.other_contacts.count(), - 0, - ) - - # Verify that the 'other' contact object still exists - domain_info = DomainInformation.objects.get() - self.assertEqual( - domain_info.other_contacts.count(), - 1, - ) - self.assertEqual( - domain_info.other_contacts.all()[0].first_name, - "Testy2", - ) - - self.assertEquals( - application.no_other_contacts_rationale, - "Hello again!", - ) - - def test_if_yes_no_form_is_no_then_no_other_contacts_required(self): - """Applicants with no other contacts have to give a reason.""" - other_contacts_page = self.app.get(reverse("application:other_contacts")) - other_contacts_form = other_contacts_page.forms[0] - other_contacts_form["other_contacts-has_other_contacts"] = "False" - response = other_contacts_page.forms[0].submit() - - # The textarea for no other contacts returns this error message - # Assert that it is returned, ie the no other contacts form is required - self.assertContains(response, "Rationale for no other employees is required.") - - # The first name field for other contacts returns this error message - # Assert that it is not returned, ie the contacts form is not required - self.assertNotContains(response, "Enter the first name / given name of this contact.") - - def test_if_yes_no_form_is_yes_then_other_contacts_required(self): - """Applicants with other contacts do not have to give a reason.""" - other_contacts_page = self.app.get(reverse("application:other_contacts")) - other_contacts_form = other_contacts_page.forms[0] - other_contacts_form["other_contacts-has_other_contacts"] = "True" - response = other_contacts_page.forms[0].submit() - - # The textarea for no other contacts returns this error message - # Assert that it is not returned, ie the no other contacts form is not required - self.assertNotContains(response, "Rationale for no other employees is required.") - - # The first name field for other contacts returns this error message - # Assert that it is returned, ie the contacts form is required - self.assertContains(response, "Enter the first name / given name of this contact.") - - def test_delete_other_contact(self): - """Other contacts can be deleted after being saved to database. - - This formset uses the DJANGO DELETE widget. We'll test that by setting 2 contacts on an application, - loading the form and marking one contact up for deletion.""" - # Populate the database with a domain application that - # has 2 "other contact" assigned to it - # We'll do it from scratch so we can reuse the other contact - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - you, _ = Contact.objects.get_or_create( - first_name="Testy you", - last_name="Tester you", - title="Admin Tester", - email="testy-admin@town.com", - phone="(201) 555 5556", - ) - other, _ = Contact.objects.get_or_create( - first_name="Testy2", - last_name="Tester2", - title="Another Tester", - email="testy2@town.com", - phone="(201) 555 5557", - ) - other2, _ = Contact.objects.get_or_create( - first_name="Testy3", - last_name="Tester3", - title="Another Tester", - email="testy3@town.com", - phone="(201) 555 5557", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - submitter=you, - creator=self.user, - status="started", - ) - application.other_contacts.add(other) - application.other_contacts.add(other2) - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - - # Minimal check to ensure the form is loaded with both other contacts - self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2") - self.assertEqual(other_contacts_form["other_contacts-1-first_name"].value, "Testy3") - - # Mark the first dude for deletion - other_contacts_form.set("other_contacts-0-DELETE", "on") - - # Submit the form - other_contacts_form.submit() - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - # Verify that the first dude was deleted - application = DomainApplication.objects.get() - self.assertEqual(application.other_contacts.count(), 1) - self.assertEqual(application.other_contacts.first().first_name, "Testy3") - - def test_delete_other_contact_does_not_allow_zero_contacts(self): - """Delete Other Contact does not allow submission with zero contacts.""" - # Populate the database with a domain application that - # has 1 "other contact" assigned to it - # We'll do it from scratch so we can reuse the other contact - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - you, _ = Contact.objects.get_or_create( - first_name="Testy you", - last_name="Tester you", - title="Admin Tester", - email="testy-admin@town.com", - phone="(201) 555 5556", - ) - other, _ = Contact.objects.get_or_create( - first_name="Testy2", - last_name="Tester2", - title="Another Tester", - email="testy2@town.com", - phone="(201) 555 5557", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - submitter=you, - creator=self.user, - status="started", - ) - application.other_contacts.add(other) - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - - # Minimal check to ensure the form is loaded - self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2") - - # Mark the first dude for deletion - other_contacts_form.set("other_contacts-0-DELETE", "on") - - # Submit the form - other_contacts_form.submit() - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - # Verify that the contact was not deleted - application = DomainApplication.objects.get() - self.assertEqual(application.other_contacts.count(), 1) - self.assertEqual(application.other_contacts.first().first_name, "Testy2") - - def test_delete_other_contact_sets_visible_empty_form_as_required_after_failed_submit(self): - """When you: - 1. add an empty contact, - 2. delete existing contacts, - 3. then submit, - The forms on page reload shows all the required fields and their errors.""" - - # Populate the database with a domain application that - # has 1 "other contact" assigned to it - # We'll do it from scratch so we can reuse the other contact - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - you, _ = Contact.objects.get_or_create( - first_name="Testy you", - last_name="Tester you", - title="Admin Tester", - email="testy-admin@town.com", - phone="(201) 555 5556", - ) - other, _ = Contact.objects.get_or_create( - first_name="Testy2", - last_name="Tester2", - title="Another Tester", - email="testy2@town.com", - phone="(201) 555 5557", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - submitter=you, - creator=self.user, - status="started", - ) - application.other_contacts.add(other) - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - - # Minimal check to ensure the form is loaded - self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2") - - # Set total forms to 2 indicating an additional formset was added. - # Submit no data though for the second formset. - # Set the first formset to be deleted. - other_contacts_form["other_contacts-TOTAL_FORMS"] = "2" - other_contacts_form.set("other_contacts-0-DELETE", "on") - - response = other_contacts_form.submit() - - # Assert that the response presents errors to the user, including to - # Enter the first name ... - self.assertContains(response, "Enter the first name / given name of this contact.") - - def test_edit_other_contact_in_place(self): - """When you: - 1. edit an existing contact which is not joined to another model, - 2. then submit, - The application is linked to the existing contact, and the existing contact updated.""" - - # Populate the database with a domain application that - # has 1 "other contact" assigned to it - # We'll do it from scratch - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - you, _ = Contact.objects.get_or_create( - first_name="Testy you", - last_name="Tester you", - title="Admin Tester", - email="testy-admin@town.com", - phone="(201) 555 5556", - ) - other, _ = Contact.objects.get_or_create( - first_name="Testy2", - last_name="Tester2", - title="Another Tester", - email="testy2@town.com", - phone="(201) 555 5557", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - submitter=you, - creator=self.user, - status="started", - ) - application.other_contacts.add(other) - - # other_contact_pk is the initial pk of the other contact. set it before update - # to be able to verify after update that the same contact object is in place - other_contact_pk = other.id - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - - # Minimal check to ensure the form is loaded - self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2") - - # update the first name of the contact - other_contacts_form["other_contacts-0-first_name"] = "Testy3" - - # Submit the updated form - other_contacts_form.submit() - - application.refresh_from_db() - - # assert that the Other Contact is updated "in place" - other_contact = application.other_contacts.all()[0] - self.assertEquals(other_contact_pk, other_contact.id) - self.assertEquals("Testy3", other_contact.first_name) - - def test_edit_other_contact_creates_new(self): - """When you: - 1. edit an existing contact which IS joined to another model, - 2. then submit, - The application is linked to a new contact, and the new contact is updated.""" - - # Populate the database with a domain application that - # has 1 "other contact" assigned to it, the other contact is also - # the authorizing official initially - # We'll do it from scratch - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - you, _ = Contact.objects.get_or_create( - first_name="Testy you", - last_name="Tester you", - title="Admin Tester", - email="testy-admin@town.com", - phone="(201) 555 5556", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - submitter=you, - creator=self.user, - status="started", - ) - application.other_contacts.add(ao) - - # other_contact_pk is the initial pk of the other contact. set it before update - # to be able to verify after update that the ao contact is still in place - # and not updated, and that the new contact has a new id - other_contact_pk = ao.id - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - - # Minimal check to ensure the form is loaded - self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy") - - # update the first name of the contact - other_contacts_form["other_contacts-0-first_name"] = "Testy2" - - # Submit the updated form - other_contacts_form.submit() - - application.refresh_from_db() - - # assert that other contact info is updated, and that a new Contact - # is created for the other contact - other_contact = application.other_contacts.all()[0] - self.assertNotEquals(other_contact_pk, other_contact.id) - self.assertEquals("Testy2", other_contact.first_name) - # assert that the authorizing official is not updated - authorizing_official = application.authorizing_official - self.assertEquals("Testy", authorizing_official.first_name) - - def test_edit_authorizing_official_in_place(self): - """When you: - 1. edit an authorizing official which is not joined to another model, - 2. then submit, - The application is linked to the existing ao, and the ao updated.""" - - # Populate the database with a domain application that - # has an authorizing_official (ao) - # We'll do it from scratch - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - creator=self.user, - status="started", - ) - - # ao_pk is the initial pk of the Authorizing Official. set it before update - # to be able to verify after update that the same Contact object is in place - ao_pk = ao.id - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - ao_page = self.app.get(reverse("application:authorizing_official")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - ao_form = ao_page.forms[0] - - # Minimal check to ensure the form is loaded - self.assertEqual(ao_form["authorizing_official-first_name"].value, "Testy") - - # update the first name of the contact - ao_form["authorizing_official-first_name"] = "Testy2" - - # Submit the updated form - ao_form.submit() - - application.refresh_from_db() - - # assert AO is updated "in place" - updated_ao = application.authorizing_official - self.assertEquals(ao_pk, updated_ao.id) - self.assertEquals("Testy2", updated_ao.first_name) - - def test_edit_authorizing_official_creates_new(self): - """When you: - 1. edit an existing authorizing official which IS joined to another model, - 2. then submit, - The application is linked to a new Contact, and the new Contact is updated.""" - - # Populate the database with a domain application that - # has authorizing official assigned to it, the authorizing offical is also - # an other contact initially - # We'll do it from scratch - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - creator=self.user, - status="started", - ) - application.other_contacts.add(ao) - - # ao_pk is the initial pk of the authorizing official. set it before update - # to be able to verify after update that the other contact is still in place - # and not updated, and that the new ao has a new id - ao_pk = ao.id - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - ao_page = self.app.get(reverse("application:authorizing_official")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - ao_form = ao_page.forms[0] - - # Minimal check to ensure the form is loaded - self.assertEqual(ao_form["authorizing_official-first_name"].value, "Testy") - - # update the first name of the contact - ao_form["authorizing_official-first_name"] = "Testy2" - - # Submit the updated form - ao_form.submit() - - application.refresh_from_db() - - # assert that the other contact is not updated - other_contacts = application.other_contacts.all() - other_contact = other_contacts[0] - self.assertEquals(ao_pk, other_contact.id) - self.assertEquals("Testy", other_contact.first_name) - # assert that the authorizing official is updated - authorizing_official = application.authorizing_official - self.assertEquals("Testy2", authorizing_official.first_name) - - def test_edit_submitter_in_place(self): - """When you: - 1. edit a submitter (your contact) which is not joined to another model, - 2. then submit, - The application is linked to the existing submitter, and the submitter updated.""" - - # Populate the database with a domain application that - # has a submitter - # We'll do it from scratch - you, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - submitter=you, - creator=self.user, - status="started", - ) - - # submitter_pk is the initial pk of the submitter. set it before update - # to be able to verify after update that the same contact object is in place - submitter_pk = you.id - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - your_contact_page = self.app.get(reverse("application:your_contact")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - your_contact_form = your_contact_page.forms[0] - - # Minimal check to ensure the form is loaded - self.assertEqual(your_contact_form["your_contact-first_name"].value, "Testy") - - # update the first name of the contact - your_contact_form["your_contact-first_name"] = "Testy2" - - # Submit the updated form - your_contact_form.submit() - - application.refresh_from_db() - - updated_submitter = application.submitter - self.assertEquals(submitter_pk, updated_submitter.id) - self.assertEquals("Testy2", updated_submitter.first_name) - - def test_edit_submitter_creates_new(self): - """When you: - 1. edit an existing your contact which IS joined to another model, - 2. then submit, - The application is linked to a new Contact, and the new Contact is updated.""" - - # Populate the database with a domain application that - # has submitter assigned to it, the submitter is also - # an other contact initially - # We'll do it from scratch - submitter, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - submitter=submitter, - creator=self.user, - status="started", - ) - application.other_contacts.add(submitter) - - # submitter_pk is the initial pk of the your contact. set it before update - # to be able to verify after update that the other contact is still in place - # and not updated, and that the new submitter has a new id - submitter_pk = submitter.id - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - your_contact_page = self.app.get(reverse("application:your_contact")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - your_contact_form = your_contact_page.forms[0] - - # Minimal check to ensure the form is loaded - self.assertEqual(your_contact_form["your_contact-first_name"].value, "Testy") - - # update the first name of the contact - your_contact_form["your_contact-first_name"] = "Testy2" - - # Submit the updated form - your_contact_form.submit() - - application.refresh_from_db() - - # assert that the other contact is not updated - other_contacts = application.other_contacts.all() - other_contact = other_contacts[0] - self.assertEquals(submitter_pk, other_contact.id) - self.assertEquals("Testy", other_contact.first_name) - # assert that the submitter is updated - submitter = application.submitter - self.assertEquals("Testy2", submitter.first_name) - - def test_application_about_your_organiztion_interstate(self): - """Special districts have to answer an additional question.""" - intro_page = self.app.get(reverse("application:")) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.INTERSTATE - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - contact_page = type_result.follow() - - self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION]) - - def test_application_tribal_government(self): - """Tribal organizations have to answer an additional question.""" - intro_page = self.app.get(reverse("application:")) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.TRIBAL - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - # the tribal government page comes immediately afterwards - self.assertIn("/tribal_government", type_result.headers["Location"]) - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - tribal_government_page = type_result.follow() - - # and the step is on the sidebar list. - self.assertContains(tribal_government_page, self.TITLES[Step.TRIBAL_GOVERNMENT]) - - def test_application_ao_dynamic_text(self): - intro_page = self.app.get(reverse("application:")) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - # ---- TYPE PAGE ---- - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "federal" - - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - - # ---- FEDERAL BRANCH PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - federal_page = type_result.follow() - federal_form = federal_page.forms[0] - federal_form["organization_federal-federal_type"] = "executive" - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - federal_result = federal_form.submit() - - # ---- ORG CONTACT PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - org_contact_page = federal_result.follow() - org_contact_form = org_contact_page.forms[0] - # federal agency so we have to fill in federal_agency - org_contact_form["organization_contact-federal_agency"] = "General Services Administration" - org_contact_form["organization_contact-organization_name"] = "Testorg" - org_contact_form["organization_contact-address_line1"] = "address 1" - org_contact_form["organization_contact-address_line2"] = "address 2" - org_contact_form["organization_contact-city"] = "NYC" - org_contact_form["organization_contact-state_territory"] = "NY" - org_contact_form["organization_contact-zipcode"] = "10002" - org_contact_form["organization_contact-urbanization"] = "URB Royal Oaks" - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - org_contact_result = org_contact_form.submit() - - # ---- AO CONTACT PAGE ---- - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_page = org_contact_result.follow() - self.assertContains(ao_page, "Executive branch federal agencies") - - # Go back to organization type page and change type - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_page.click(str(self.TITLES["organization_type"]), index=0) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_form["organization_type-organization_type"] = "city" - type_result = type_form.submit() - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - election_page = type_result.follow() - - # Go back to AO page and test the dynamic text changed - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_page = election_page.click(str(self.TITLES["authorizing_official"]), index=0) - self.assertContains(ao_page, "Domain requests from cities") - - def test_application_dotgov_domain_dynamic_text(self): - intro_page = self.app.get(reverse("application:")) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - # ---- TYPE PAGE ---- - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "federal" - - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - - # ---- FEDERAL BRANCH PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - federal_page = type_result.follow() - federal_form = federal_page.forms[0] - federal_form["organization_federal-federal_type"] = "executive" - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - federal_result = federal_form.submit() - - # ---- ORG CONTACT PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - org_contact_page = federal_result.follow() - org_contact_form = org_contact_page.forms[0] - # federal agency so we have to fill in federal_agency - org_contact_form["organization_contact-federal_agency"] = "General Services Administration" - org_contact_form["organization_contact-organization_name"] = "Testorg" - org_contact_form["organization_contact-address_line1"] = "address 1" - org_contact_form["organization_contact-address_line2"] = "address 2" - org_contact_form["organization_contact-city"] = "NYC" - org_contact_form["organization_contact-state_territory"] = "NY" - org_contact_form["organization_contact-zipcode"] = "10002" - org_contact_form["organization_contact-urbanization"] = "URB Royal Oaks" - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - org_contact_result = org_contact_form.submit() - - # ---- AO CONTACT PAGE ---- - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_page = org_contact_result.follow() - - # ---- AUTHORIZING OFFICIAL PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_page = org_contact_result.follow() - ao_form = ao_page.forms[0] - ao_form["authorizing_official-first_name"] = "Testy ATO" - ao_form["authorizing_official-last_name"] = "Tester ATO" - ao_form["authorizing_official-title"] = "Chief Tester" - ao_form["authorizing_official-email"] = "testy@town.com" - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_result = ao_form.submit() - - # ---- CURRENT SITES PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - current_sites_page = ao_result.follow() - current_sites_form = current_sites_page.forms[0] - current_sites_form["current_sites-0-website"] = "www.city.com" - - # test saving the page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - current_sites_result = current_sites_form.submit() - - # ---- DOTGOV DOMAIN PAGE ---- - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - dotgov_page = current_sites_result.follow() - - self.assertContains(dotgov_page, "medicare.gov") - - # Go back to organization type page and change type - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - dotgov_page.click(str(self.TITLES["organization_type"]), index=0) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_form["organization_type-organization_type"] = "city" - type_result = type_form.submit() - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - election_page = type_result.follow() - - # Go back to dotgov domain page to test the dynamic text changed - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - dotgov_page = election_page.click(str(self.TITLES["dotgov_domain"]), index=0) - self.assertContains(dotgov_page, "CityofEudoraKS.gov") - self.assertNotContains(dotgov_page, "medicare.gov") - - def test_application_formsets(self): - """Users are able to add more than one of some fields.""" - current_sites_page = self.app.get(reverse("application:current_sites")) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - # fill in the form field - current_sites_form = current_sites_page.forms[0] - self.assertIn("current_sites-0-website", current_sites_form.fields) - self.assertNotIn("current_sites-1-website", current_sites_form.fields) - current_sites_form["current_sites-0-website"] = "https://example.com" - - # click "Add another" - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - current_sites_result = current_sites_form.submit("submit_button", value="save") - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - current_sites_form = current_sites_result.follow().forms[0] - - # verify that there are two form fields - value = current_sites_form["current_sites-0-website"].value - self.assertEqual(value, "https://example.com") - self.assertIn("current_sites-1-website", current_sites_form.fields) - # and it is correctly referenced in the ManyToOne relationship - application = DomainApplication.objects.get() # there's only one - self.assertEqual( - application.current_websites.filter(website="https://example.com").count(), - 1, - ) - - @skip("WIP") - def test_application_edit_restore(self): - """ - Test that a previously saved application is available at the /edit endpoint. - """ - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(555) 555 5555", - ) - domain, _ = Domain.objects.get_or_create(name="city.gov") - alt, _ = Website.objects.get_or_create(website="city1.gov") - current, _ = Website.objects.get_or_create(website="city.com") - you, _ = Contact.objects.get_or_create( - first_name="Testy you", - last_name="Tester you", - title="Admin Tester", - email="testy-admin@town.com", - phone="(555) 555 5556", - ) - other, _ = Contact.objects.get_or_create( - first_name="Testy2", - last_name="Tester2", - title="Another Tester", - email="testy2@town.com", - phone="(555) 555 5557", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - requested_domain=domain, - submitter=you, - creator=self.user, - ) - application.other_contacts.add(other) - application.current_websites.add(current) - application.alternative_domains.add(alt) - - # prime the form by visiting /edit - url = reverse("edit-application", kwargs={"id": application.pk}) - response = self.client.get(url) - - # TODO: this is a sketch of each page in the wizard which needs to be tested - # Django does not have tools sufficient for real end to end integration testing - # (for example, USWDS moves radio buttons off screen and replaces them with - # CSS styled "fakes" -- Django cannot determine if those are visually correct) - # -- the best that can/should be done here is to ensure the correct values - # are being passed to the templating engine - - url = reverse("application:organization_type") - response = self.client.get(url, follow=True) - self.assertContains(response, "") - # choices = response.context['wizard']['form']['organization_type'].subwidgets - # radio = [ x for x in choices if x.data["value"] == "federal" ][0] - # checked = radio.data["selected"] - # self.assertTrue(checked) - - # url = reverse("application:organization_federal") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:organization_contact") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:authorizing_official") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:current_sites") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:dotgov_domain") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:purpose") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:your_contact") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:other_contacts") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:other_contacts") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:security_email") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:anything_else") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:requirements") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - def test_long_org_name_in_application(self): - """ - Make sure the long name is displaying in the application form, - org step - """ - intro_page = self.app.get(reverse("application:")) - # django-webtest does not handle cookie-based sessions well because it keeps - # resetting the session key on each new request, thus destroying the concept - # of a "session". We are going to do it manually, saving the session ID here - # and then setting the cookie on each request. - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - - self.assertContains(type_page, "Federal: an agency of the U.S. government") - - def test_long_org_name_in_application_manage(self): - """ - Make sure the long name is displaying in the application summary - page (manage your application) - """ - completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) - home_page = self.app.get("/") - self.assertContains(home_page, "city.gov") - # click the "Edit" link - detail_page = home_page.click("Manage", index=0) - self.assertContains(detail_page, "Federal: an agency of the U.S. government") - - def test_submit_modal_no_domain_text_fallback(self): - """When user clicks on submit your domain request and the requested domain - is null (possible through url direct access to the review page), present - fallback copy in the modal's header. - - NOTE: This may be a moot point if we implement a more solid pattern in the - future, like not a submit action at all on the review page.""" - - review_page = self.app.get(reverse("application:review")) - self.assertContains(review_page, "toggle-submit-domain-request") - self.assertContains(review_page, "You are about to submit an incomplete request") - - -class TestWithDomainPermissions(TestWithUser): - def setUp(self): - super().setUp() - self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") - self.domain_with_ip, _ = Domain.objects.get_or_create(name="nameserverwithip.gov") - self.domain_just_nameserver, _ = Domain.objects.get_or_create(name="justnameserver.com") - self.domain_no_information, _ = Domain.objects.get_or_create(name="noinformation.gov") - self.domain_on_hold, _ = Domain.objects.get_or_create( - name="on-hold.gov", - state=Domain.State.ON_HOLD, - expiration_date=timezone.make_aware( - datetime.combine(date.today() + timedelta(days=1), datetime.min.time()) - ), - ) - self.domain_deleted, _ = Domain.objects.get_or_create( - name="deleted.gov", - state=Domain.State.DELETED, - expiration_date=timezone.make_aware( - datetime.combine(date.today() + timedelta(days=1), datetime.min.time()) - ), - ) - - self.domain_dsdata, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") - self.domain_multdsdata, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov") - # We could simply use domain (igorville) but this will be more readable in tests - # that inherit this setUp - self.domain_dnssec_none, _ = Domain.objects.get_or_create(name="dnssec-none.gov") - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dsdata) - DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_multdsdata) - DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dnssec_none) - DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_ip) - DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_just_nameserver) - DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_on_hold) - DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_deleted) - - self.role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER - ) - - UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.MANAGER - ) - UserDomainRole.objects.get_or_create( - user=self.user, - domain=self.domain_multdsdata, - role=UserDomainRole.Roles.MANAGER, - ) - UserDomainRole.objects.get_or_create( - user=self.user, - domain=self.domain_dnssec_none, - role=UserDomainRole.Roles.MANAGER, - ) - UserDomainRole.objects.get_or_create( - user=self.user, - domain=self.domain_with_ip, - role=UserDomainRole.Roles.MANAGER, - ) - UserDomainRole.objects.get_or_create( - user=self.user, - domain=self.domain_just_nameserver, - role=UserDomainRole.Roles.MANAGER, - ) - UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain_on_hold, role=UserDomainRole.Roles.MANAGER - ) - UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain_deleted, role=UserDomainRole.Roles.MANAGER - ) - - def tearDown(self): - try: - UserDomainRole.objects.all().delete() - if hasattr(self.domain, "contacts"): - self.domain.contacts.all().delete() - DomainApplication.objects.all().delete() - DomainInformation.objects.all().delete() - PublicContact.objects.all().delete() - HostIP.objects.all().delete() - Host.objects.all().delete() - Domain.objects.all().delete() - UserDomainRole.objects.all().delete() - except ValueError: # pass if already deleted - pass - super().tearDown() - - -class TestDomainPermissions(TestWithDomainPermissions): - def test_not_logged_in(self): - """Not logged in gets a redirect to Login.""" - for view_name in [ - "domain", - "domain-users", - "domain-users-add", - "domain-dns-nameservers", - "domain-org-name-address", - "domain-authorizing-official", - "domain-your-contact-information", - "domain-security-email", - ]: - with self.subTest(view_name=view_name): - response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id})) - self.assertEqual(response.status_code, 302) - - def test_no_domain_role(self): - """Logged in but no role gets 403 Forbidden.""" - self.client.force_login(self.user) - self.role.delete() # user no longer has a role on this domain - - for view_name in [ - "domain", - "domain-users", - "domain-users-add", - "domain-dns-nameservers", - "domain-org-name-address", - "domain-authorizing-official", - "domain-your-contact-information", - "domain-security-email", - ]: - with self.subTest(view_name=view_name): - with less_console_noise(): - response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id})) - self.assertEqual(response.status_code, 403) - - def test_domain_pages_blocked_for_on_hold_and_deleted(self): - """Test that the domain pages are blocked for on hold and deleted domains""" - - self.client.force_login(self.user) - for view_name in [ - "domain-users", - "domain-users-add", - "domain-dns", - "domain-dns-nameservers", - "domain-dns-dnssec", - "domain-dns-dnssec-dsdata", - "domain-org-name-address", - "domain-authorizing-official", - "domain-your-contact-information", - "domain-security-email", - ]: - for domain in [ - self.domain_on_hold, - self.domain_deleted, - ]: - with self.subTest(view_name=view_name, domain=domain): - with less_console_noise(): - response = self.client.get(reverse(view_name, kwargs={"pk": domain.id})) - self.assertEqual(response.status_code, 403) - - -class TestDomainOverview(TestWithDomainPermissions, WebTest): - def setUp(self): - super().setUp() - self.app.set_user(self.user.username) - self.client.force_login(self.user) - - -class TestDomainDetail(TestDomainOverview): - @skip("Assertion broke for no reason, why? Need to fix") - def test_domain_detail_link_works(self): - home_page = self.app.get("/") - logger.info(f"This is the value of home_page: {home_page}") - self.assertContains(home_page, "igorville.gov") - # click the "Edit" link - detail_page = home_page.click("Manage", index=0) - self.assertContains(detail_page, "igorville.gov") - self.assertContains(detail_page, "Status") - - def test_unknown_domain_does_not_show_as_expired_on_homepage(self): - """An UNKNOWN domain does not show as expired on the homepage. - It shows as 'DNS needed'""" - # At the time of this test's writing, there are 6 UNKNOWN domains inherited - # from constructors. Let's reset. - Domain.objects.all().delete() - UserDomainRole.objects.all().delete() - self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") - home_page = self.app.get("/") - self.assertNotContains(home_page, "igorville.gov") - self.role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER - ) - home_page = self.app.get("/") - self.assertContains(home_page, "igorville.gov") - igorville = Domain.objects.get(name="igorville.gov") - self.assertEquals(igorville.state, Domain.State.UNKNOWN) - self.assertNotContains(home_page, "Expired") - self.assertContains(home_page, "DNS needed") - - def test_unknown_domain_does_not_show_as_expired_on_detail_page(self): - """An UNKNOWN domain does not show as expired on the detail page. - It shows as 'DNS needed'""" - # At the time of this test's writing, there are 6 UNKNOWN domains inherited - # from constructors. Let's reset. - Domain.objects.all().delete() - UserDomainRole.objects.all().delete() - - self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - self.role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER - ) - - home_page = self.app.get("/") - self.assertContains(home_page, "igorville.gov") - igorville = Domain.objects.get(name="igorville.gov") - self.assertEquals(igorville.state, Domain.State.UNKNOWN) - detail_page = home_page.click("Manage", index=0) - self.assertNotContains(detail_page, "Expired") - - self.assertContains(detail_page, "DNS needed") - - def test_domain_detail_blocked_for_ineligible_user(self): - """We could easily duplicate this test for all domain management - views, but a single url test should be solid enough since all domain - management pages share the same permissions class""" - self.user.status = User.RESTRICTED - self.user.save() - home_page = self.app.get("/") - self.assertContains(home_page, "igorville.gov") - with less_console_noise(): - response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) - self.assertEqual(response.status_code, 403) - - def test_domain_detail_allowed_for_on_hold(self): - """Test that the domain overview page displays for on hold domain""" - home_page = self.app.get("/") - self.assertContains(home_page, "on-hold.gov") - - # View domain overview page - detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_on_hold.id})) - self.assertNotContains(detail_page, "Edit") - - def test_domain_detail_see_just_nameserver(self): - home_page = self.app.get("/") - self.assertContains(home_page, "justnameserver.com") - - # View nameserver on Domain Overview page - detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_just_nameserver.id})) - - self.assertContains(detail_page, "justnameserver.com") - self.assertContains(detail_page, "ns1.justnameserver.com") - self.assertContains(detail_page, "ns2.justnameserver.com") - - def test_domain_detail_see_nameserver_and_ip(self): - home_page = self.app.get("/") - self.assertContains(home_page, "nameserverwithip.gov") - - # View nameserver on Domain Overview page - detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_with_ip.id})) - - self.assertContains(detail_page, "nameserverwithip.gov") - - self.assertContains(detail_page, "ns1.nameserverwithip.gov") - self.assertContains(detail_page, "ns2.nameserverwithip.gov") - self.assertContains(detail_page, "ns3.nameserverwithip.gov") - # Splitting IP addresses bc there is odd whitespace and can't strip text - self.assertContains(detail_page, "(1.2.3.4,") - self.assertContains(detail_page, "2.3.4.5)") - - def test_domain_detail_with_no_information_or_application(self): - """Test that domain management page returns 200 and displays error - when no domain information or domain application exist""" - # have to use staff user for this test - staff_user = create_user() - # staff_user.save() - self.client.force_login(staff_user) - - # need to set the analyst_action and analyst_action_location - # in the session to emulate user clicking Manage Domain - # in the admin interface - session = self.client.session - session["analyst_action"] = "foo" - session["analyst_action_location"] = self.domain_no_information.id - session.save() - - detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_no_information.id})) - - self.assertContains(detail_page, "noinformation.gov") - self.assertContains(detail_page, "Domain missing domain information") - - -class TestDomainManagers(TestDomainOverview): - def tearDown(self): - """Ensure that the user has its original permissions""" - super().tearDown() - self.user.is_staff = False - self.user.save() - - def test_domain_managers(self): - response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) - self.assertContains(response, "Domain managers") - - def test_domain_managers_add_link(self): - """Button to get to user add page works.""" - management_page = self.app.get(reverse("domain-users", kwargs={"pk": self.domain.id})) - add_page = management_page.click("Add a domain manager") - self.assertContains(add_page, "Add a domain manager") - - def test_domain_user_add(self): - response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - self.assertContains(response, "Add a domain manager") - - @boto3_mocking.patching - def test_domain_user_add_form(self): - """Adding an existing user works.""" - other_user, _ = get_user_model().objects.get_or_create(email="mayor@igorville.gov") - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - add_page.form["email"] = "mayor@igorville.gov" - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - success_result = add_page.form.submit() - - self.assertEqual(success_result.status_code, 302) - self.assertEqual( - success_result["Location"], - reverse("domain-users", kwargs={"pk": self.domain.id}), - ) - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - success_page = success_result.follow() - self.assertContains(success_page, "mayor@igorville.gov") - - @boto3_mocking.patching - def test_domain_invitation_created(self): - """Add user on a nonexistent email creates an invitation. - - Adding a non-existent user sends an email as a side-effect, so mock - out the boto3 SES email sending here. - """ - # make sure there is no user with this email - email_address = "mayor@igorville.gov" - User.objects.filter(email=email_address).delete() - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - success_result = add_page.form.submit() - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - success_page = success_result.follow() - - self.assertContains(success_page, email_address) - self.assertContains(success_page, "Cancel") # link to cancel invitation - self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists()) - - @boto3_mocking.patching - def test_domain_invitation_created_for_caps_email(self): - """Add user on a nonexistent email with CAPS creates an invitation to lowercase email. - - Adding a non-existent user sends an email as a side-effect, so mock - out the boto3 SES email sending here. - """ - # make sure there is no user with this email - email_address = "mayor@igorville.gov" - caps_email_address = "MAYOR@igorville.gov" - User.objects.filter(email=email_address).delete() - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = caps_email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - success_result = add_page.form.submit() - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - success_page = success_result.follow() - - self.assertContains(success_page, email_address) - self.assertContains(success_page, "Cancel") # link to cancel invitation - self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists()) - - @boto3_mocking.patching - def test_domain_invitation_email_sent(self): - """Inviting a non-existent user sends them an email.""" - # make sure there is no user with this email - email_address = "mayor@igorville.gov" - User.objects.filter(email=email_address).delete() - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - mock_client = MagicMock() - mock_client_instance = mock_client.return_value - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit() - - # check the mock instance to see if `send_email` was called right - mock_client_instance.send_email.assert_called_once_with( - FromEmailAddress=settings.DEFAULT_FROM_EMAIL, - Destination={"ToAddresses": [email_address]}, - Content=ANY, - ) - - @boto3_mocking.patching - def test_domain_invitation_email_has_email_as_requester_non_existent(self): - """Inviting a non existent user sends them an email, with email as the name.""" - # make sure there is no user with this email - email_address = "mayor@igorville.gov" - User.objects.filter(email=email_address).delete() - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - mock_client = MagicMock() - mock_client_instance = mock_client.return_value - - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit() - - # check the mock instance to see if `send_email` was called right - mock_client_instance.send_email.assert_called_once_with( - FromEmailAddress=settings.DEFAULT_FROM_EMAIL, - Destination={"ToAddresses": [email_address]}, - Content=ANY, - ) - - # Check the arguments passed to send_email method - _, kwargs = mock_client_instance.send_email.call_args - - # Extract the email content, and check that the message is as we expect - email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] - self.assertIn("info@example.com", email_content) - - # Check that the requesters first/last name do not exist - self.assertNotIn("First", email_content) - self.assertNotIn("Last", email_content) - self.assertNotIn("First Last", email_content) - - @boto3_mocking.patching - def test_domain_invitation_email_has_email_as_requester(self): - """Inviting a user sends them an email, with email as the name.""" - # Create a fake user object - email_address = "mayor@igorville.gov" - User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com") - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - mock_client = MagicMock() - mock_client_instance = mock_client.return_value - - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit() - - # check the mock instance to see if `send_email` was called right - mock_client_instance.send_email.assert_called_once_with( - FromEmailAddress=settings.DEFAULT_FROM_EMAIL, - Destination={"ToAddresses": [email_address]}, - Content=ANY, - ) - - # Check the arguments passed to send_email method - _, kwargs = mock_client_instance.send_email.call_args - - # Extract the email content, and check that the message is as we expect - email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] - self.assertIn("info@example.com", email_content) - - # Check that the requesters first/last name do not exist - self.assertNotIn("First", email_content) - self.assertNotIn("Last", email_content) - self.assertNotIn("First Last", email_content) - - @boto3_mocking.patching - def test_domain_invitation_email_has_email_as_requester_staff(self): - """Inviting a user sends them an email, with email as the name.""" - # Create a fake user object - email_address = "mayor@igorville.gov" - User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com") - - # Make sure the user is staff - self.user.is_staff = True - self.user.save() - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - mock_client = MagicMock() - mock_client_instance = mock_client.return_value - - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit() - - # check the mock instance to see if `send_email` was called right - mock_client_instance.send_email.assert_called_once_with( - FromEmailAddress=settings.DEFAULT_FROM_EMAIL, - Destination={"ToAddresses": [email_address]}, - Content=ANY, - ) - - # Check the arguments passed to send_email method - _, kwargs = mock_client_instance.send_email.call_args - - # Extract the email content, and check that the message is as we expect - email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] - self.assertIn("help@get.gov", email_content) - - # Check that the requesters first/last name do not exist - self.assertNotIn("First", email_content) - self.assertNotIn("Last", email_content) - self.assertNotIn("First Last", email_content) - - @boto3_mocking.patching - def test_domain_invitation_email_displays_error_non_existent(self): - """Inviting a non existent user sends them an email, with email as the name.""" - # make sure there is no user with this email - email_address = "mayor@igorville.gov" - User.objects.filter(email=email_address).delete() - - # Give the user who is sending the email an invalid email address - self.user.email = "" - self.user.save() - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - mock_client = MagicMock() - mock_error_message = MagicMock() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with patch("django.contrib.messages.error") as mock_error_message: - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit().follow() - - expected_message_content = "Can't send invitation email. No email is associated with your account." - - # Grab the message content - returned_error_message = mock_error_message.call_args[0][1] - - # Check that the message content is what we expect - self.assertEqual(expected_message_content, returned_error_message) - - @boto3_mocking.patching - def test_domain_invitation_email_displays_error(self): - """When the requesting user has no email, an error is displayed""" - # make sure there is no user with this email - # Create a fake user object - email_address = "mayor@igorville.gov" - User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com") - - # Give the user who is sending the email an invalid email address - self.user.email = "" - self.user.save() - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - mock_client = MagicMock() - - mock_error_message = MagicMock() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with patch("django.contrib.messages.error") as mock_error_message: - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit().follow() - - expected_message_content = "Can't send invitation email. No email is associated with your account." - - # Grab the message content - returned_error_message = mock_error_message.call_args[0][1] - - # Check that the message content is what we expect - self.assertEqual(expected_message_content, returned_error_message) - - def test_domain_invitation_cancel(self): - """Posting to the delete view deletes an invitation.""" - email_address = "mayor@igorville.gov" - invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address) - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) - mock_client.EMAILS_SENT.clear() - with self.assertRaises(DomainInvitation.DoesNotExist): - DomainInvitation.objects.get(id=invitation.id) - - def test_domain_invitation_cancel_no_permissions(self): - """Posting to the delete view as a different user should fail.""" - email_address = "mayor@igorville.gov" - invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address) - - other_user = User() - other_user.save() - self.client.force_login(other_user) - mock_client = MagicMock() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): # permission denied makes console errors - result = self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) - - self.assertEqual(result.status_code, 403) - - @boto3_mocking.patching - def test_domain_invitation_flow(self): - """Send an invitation to a new user, log in and load the dashboard.""" - email_address = "mayor@igorville.gov" - User.objects.filter(email=email_address).delete() - - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - mock_client = MagicMock() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - add_page.form.submit() - - # user was invited, create them - new_user = User.objects.create(username=email_address, email=email_address) - # log them in to `self.app` - self.app.set_user(new_user.username) - # and manually call the on each login callback - new_user.on_each_login() - - # Now load the home page and make sure our domain appears there - home_page = self.app.get(reverse("home")) - self.assertContains(home_page, self.domain.name) - - -class TestDomainNameservers(TestDomainOverview): - def test_domain_nameservers(self): - """Can load domain's nameservers page.""" - page = self.client.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - self.assertContains(page, "DNS name servers") - - def test_domain_nameservers_form_submit_one_nameserver(self): - """Nameserver form submitted with one nameserver throws error. - - Uses self.app WebTest because we need to interact with forms. - """ - # initial nameservers page has one server with two ips - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form with only one nameserver, should error - # regarding required fields - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the required field. form requires a minimum of 2 name servers - self.assertContains( - result, - "At least two name servers are required.", - count=2, - status_code=200, - ) - - def test_domain_nameservers_form_submit_subdomain_missing_ip(self): - """Nameserver form catches missing ip error on subdomain. - - Uses self.app WebTest because we need to interact with forms. - """ - # initial nameservers page has one server with two ips - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form without two hosts, both subdomains, - # only one has ips - nameservers_page.form["form-1-server"] = "ns2.igorville.gov" - - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the required field. subdomain missing an ip - self.assertContains( - result, - str(NameserverError(code=NameserverErrorCodes.MISSING_IP)), - count=2, - status_code=200, - ) - - def test_domain_nameservers_form_submit_missing_host(self): - """Nameserver form catches error when host is missing. - - Uses self.app WebTest because we need to interact with forms. - """ - # initial nameservers page has one server with two ips - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form without two hosts, both subdomains, - # only one has ips - nameservers_page.form["form-1-ip"] = "127.0.0.1" - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the required field. nameserver has ip but missing host - self.assertContains( - result, - str(NameserverError(code=NameserverErrorCodes.MISSING_HOST)), - count=2, - status_code=200, - ) - - def test_domain_nameservers_form_submit_duplicate_host(self): - """Nameserver form catches error when host is duplicated. - - Uses self.app WebTest because we need to interact with forms. - """ - # initial nameservers page has one server with two ips - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form with duplicate host names of fake.host.com - nameservers_page.form["form-0-ip"] = "" - nameservers_page.form["form-1-server"] = "fake.host.com" - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the required field. remove duplicate entry - self.assertContains( - result, - str(NameserverError(code=NameserverErrorCodes.DUPLICATE_HOST)), - count=2, - status_code=200, - ) - - def test_domain_nameservers_form_submit_whitespace(self): - """Nameserver form removes whitespace from ip. - - Uses self.app WebTest because we need to interact with forms. - """ - nameserver1 = "ns1.igorville.gov" - nameserver2 = "ns2.igorville.gov" - valid_ip = "1.1. 1.1" - # initial nameservers page has one server with two ips - # have to throw an error in order to test that the whitespace has been stripped from ip - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form without one host and an ip with whitespace - nameservers_page.form["form-0-server"] = nameserver1 - nameservers_page.form["form-1-ip"] = valid_ip - nameservers_page.form["form-1-server"] = nameserver2 - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a post with an ip address which has been stripped of whitespace, - # response should be a 302 to success page - self.assertEqual(result.status_code, 302) - self.assertEqual( - result["Location"], - reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}), - ) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - page = result.follow() - # in the event of a generic nameserver error from registry error, there will be a 302 - # with an error message displayed, so need to follow 302 and test for success message - self.assertContains(page, "The name servers for this domain have been updated") - - def test_domain_nameservers_form_submit_glue_record_not_allowed(self): - """Nameserver form catches error when IP is present - but host not subdomain. - - Uses self.app WebTest because we need to interact with forms. - """ - nameserver1 = "ns1.igorville.gov" - nameserver2 = "ns2.igorville.com" - valid_ip = "127.0.0.1" - # initial nameservers page has one server with two ips - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form without two hosts, both subdomains, - # only one has ips - nameservers_page.form["form-0-server"] = nameserver1 - nameservers_page.form["form-1-server"] = nameserver2 - nameservers_page.form["form-1-ip"] = valid_ip - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the required field. nameserver has ip but missing host - self.assertContains( - result, - str(NameserverError(code=NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED)), - count=2, - status_code=200, - ) - - def test_domain_nameservers_form_submit_invalid_ip(self): - """Nameserver form catches invalid IP on submission. - - Uses self.app WebTest because we need to interact with forms. - """ - nameserver = "ns2.igorville.gov" - invalid_ip = "123" - # initial nameservers page has one server with two ips - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form without two hosts, both subdomains, - # only one has ips - nameservers_page.form["form-1-server"] = nameserver - nameservers_page.form["form-1-ip"] = invalid_ip - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the required field. nameserver has ip but missing host - self.assertContains( - result, - str(NameserverError(code=NameserverErrorCodes.INVALID_IP, nameserver=nameserver)), - count=2, - status_code=200, - ) - - def test_domain_nameservers_form_submit_invalid_host(self): - """Nameserver form catches invalid host on submission. - - Uses self.app WebTest because we need to interact with forms. - """ - nameserver = "invalid-nameserver.gov" - valid_ip = "123.2.45.111" - # initial nameservers page has one server with two ips - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form without two hosts, both subdomains, - # only one has ips - nameservers_page.form["form-1-server"] = nameserver - nameservers_page.form["form-1-ip"] = valid_ip - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the required field. nameserver has invalid host - self.assertContains( - result, - str(NameserverError(code=NameserverErrorCodes.INVALID_HOST, nameserver=nameserver)), - count=2, - status_code=200, - ) - - def test_domain_nameservers_form_submits_successfully(self): - """Nameserver form submits successfully with valid input. - - Uses self.app WebTest because we need to interact with forms. - """ - nameserver1 = "ns1.igorville.gov" - nameserver2 = "ns2.igorville.gov" - valid_ip = "127.0.0.1" - # initial nameservers page has one server with two ips - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form without two hosts, both subdomains, - # only one has ips - nameservers_page.form["form-0-server"] = nameserver1 - nameservers_page.form["form-1-server"] = nameserver2 - nameservers_page.form["form-1-ip"] = valid_ip - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a successful post, response should be a 302 - self.assertEqual(result.status_code, 302) - self.assertEqual( - result["Location"], - reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}), - ) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - page = result.follow() - self.assertContains(page, "The name servers for this domain have been updated") - - def test_domain_nameservers_form_invalid(self): - """Nameserver form does not submit with invalid data. - - Uses self.app WebTest because we need to interact with forms. - """ - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # first two nameservers are required, so if we empty one out we should - # get a form error - nameservers_page.form["form-0-server"] = "" - with less_console_noise(): # swallow logged warning message - result = nameservers_page.form.submit() - # form submission was a post with an error, response should be a 200 - # error text appears four times, twice at the top of the page, - # once around each required field. - self.assertContains( - result, - "At least two name servers are required.", - count=4, - status_code=200, - ) - - -class TestDomainAuthorizingOfficial(TestDomainOverview): - def test_domain_authorizing_official(self): - """Can load domain's authorizing official page.""" - page = self.client.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) - # once on the sidebar, once in the title - self.assertContains(page, "Authorizing official", count=2) - - def test_domain_authorizing_official_content(self): - """Authorizing official information appears on the page.""" - self.domain_information.authorizing_official = Contact(first_name="Testy") - self.domain_information.authorizing_official.save() - self.domain_information.save() - page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) - self.assertContains(page, "Testy") - - def test_domain_edit_authorizing_official_in_place(self): - """When editing an authorizing official for domain information and AO is not - joined to any other objects""" - self.domain_information.authorizing_official = Contact( - first_name="Testy", last_name="Tester", title="CIO", email="nobody@igorville.gov" - ) - self.domain_information.authorizing_official.save() - self.domain_information.save() - ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_form = ao_page.forms[0] - self.assertEqual(ao_form["first_name"].value, "Testy") - ao_form["first_name"] = "Testy2" - # ao_pk is the initial pk of the authorizing official. set it before update - # to be able to verify after update that the same contact object is in place - ao_pk = self.domain_information.authorizing_official.id - ao_form.submit() - - # refresh domain information - self.domain_information.refresh_from_db() - self.assertEqual("Testy2", self.domain_information.authorizing_official.first_name) - self.assertEqual(ao_pk, self.domain_information.authorizing_official.id) - - def test_domain_edit_authorizing_official_creates_new(self): - """When editing an authorizing official for domain information and AO IS - joined to another object""" - # set AO and Other Contact to the same Contact object - self.domain_information.authorizing_official = Contact( - first_name="Testy", last_name="Tester", title="CIO", email="nobody@igorville.gov" - ) - self.domain_information.authorizing_official.save() - self.domain_information.save() - self.domain_information.other_contacts.add(self.domain_information.authorizing_official) - self.domain_information.save() - # load the Authorizing Official in the web form - ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_form = ao_page.forms[0] - # verify the first name is "Testy" and then change it to "Testy2" - self.assertEqual(ao_form["first_name"].value, "Testy") - ao_form["first_name"] = "Testy2" - # ao_pk is the initial pk of the authorizing official. set it before update - # to be able to verify after update that the same contact object is in place - ao_pk = self.domain_information.authorizing_official.id - ao_form.submit() - - # refresh domain information - self.domain_information.refresh_from_db() - # assert that AO information is updated, and that the AO is a new Contact - self.assertEqual("Testy2", self.domain_information.authorizing_official.first_name) - self.assertNotEqual(ao_pk, self.domain_information.authorizing_official.id) - # assert that the Other Contact information is not updated and that the Other Contact - # is the original Contact object - other_contact = self.domain_information.other_contacts.all()[0] - self.assertEqual("Testy", other_contact.first_name) - self.assertEqual(ao_pk, other_contact.id) - - -class TestDomainOrganization(TestDomainOverview): - def test_domain_org_name_address(self): - """Can load domain's org name and mailing address page.""" - page = self.client.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) - # once on the sidebar, once in the page title, once as H1 - self.assertContains(page, "Organization name and mailing address", count=3) - - def test_domain_org_name_address_content(self): - """Org name and address information appears on the page.""" - self.domain_information.organization_name = "Town of Igorville" - self.domain_information.save() - page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) - self.assertContains(page, "Town of Igorville") - - def test_domain_org_name_address_form(self): - """Submitting changes works on the org name address page.""" - self.domain_information.organization_name = "Town of Igorville" - self.domain_information.save() - org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - org_name_page.form["organization_name"] = "Not igorville" - org_name_page.form["city"] = "Faketown" - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - success_result_page = org_name_page.form.submit() - self.assertEqual(success_result_page.status_code, 200) - - self.assertContains(success_result_page, "Not igorville") - self.assertContains(success_result_page, "Faketown") - - -class TestDomainContactInformation(TestDomainOverview): - def test_domain_your_contact_information(self): - """Can load domain's your contact information page.""" - page = self.client.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})) - self.assertContains(page, "Your contact information") - - def test_domain_your_contact_information_content(self): - """Logged-in user's contact information appears on the page.""" - self.user.contact.first_name = "Testy" - self.user.contact.save() - page = self.app.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})) - self.assertContains(page, "Testy") - - -class TestDomainSecurityEmail(TestDomainOverview): - def test_domain_security_email_existing_security_contact(self): - """Can load domain's security email page.""" - self.mockSendPatch = patch("registrar.models.domain.registry.send") - self.mockedSendFunction = self.mockSendPatch.start() - self.mockedSendFunction.side_effect = self.mockSend - - domain_contact, _ = Domain.objects.get_or_create(name="freeman.gov") - # Add current user to this domain - _ = UserDomainRole(user=self.user, domain=domain_contact, role="admin").save() - page = self.client.get(reverse("domain-security-email", kwargs={"pk": domain_contact.id})) - - # Loads correctly - self.assertContains(page, "Security email") - self.assertContains(page, "security@mail.gov") - self.mockSendPatch.stop() - - def test_domain_security_email_no_security_contact(self): - """Loads a domain with no defined security email. - We should not show the default.""" - self.mockSendPatch = patch("registrar.models.domain.registry.send") - self.mockedSendFunction = self.mockSendPatch.start() - self.mockedSendFunction.side_effect = self.mockSend - - page = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) - - # Loads correctly - self.assertContains(page, "Security email") - self.assertNotContains(page, "dotgov@cisa.dhs.gov") - self.mockSendPatch.stop() - - def test_domain_security_email(self): - """Can load domain's security email page.""" - page = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) - self.assertContains(page, "Security email") - - def test_domain_security_email_form(self): - """Adding a security email works. - Uses self.app WebTest because we need to interact with forms. - """ - security_email_page = self.app.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - security_email_page.form["security_email"] = "mayor@igorville.gov" - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - mock_client = MagicMock() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): # swallow log warning message - result = security_email_page.form.submit() - self.assertEqual(result.status_code, 302) - self.assertEqual( - result["Location"], - reverse("domain-security-email", kwargs={"pk": self.domain.id}), - ) - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - success_page = result.follow() - self.assertContains(success_page, "The security email for this domain has been updated") - - def test_security_email_form_messages(self): - """ - Test against the success and error messages that are defined in the view - """ - p = "adminpass" - self.client.login(username="superuser", password=p) - - form_data_registry_error = { - "security_email": "test@failCreate.gov", - } - - form_data_contact_error = { - "security_email": "test@contactError.gov", - } - - form_data_success = { - "security_email": "test@something.gov", - } - - test_cases = [ - ( - "RegistryError", - form_data_registry_error, - str(GenericError(code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY)), - ), - ( - "ContactError", - form_data_contact_error, - str(SecurityEmailError(code=SecurityEmailErrorCodes.BAD_DATA)), - ), - ( - "RegistrySuccess", - form_data_success, - "The security email for this domain has been updated.", - ), - # Add more test cases with different scenarios here - ] - - for test_name, data, expected_message in test_cases: - response = self.client.post( - reverse("domain-security-email", kwargs={"pk": self.domain.id}), - data=data, - follow=True, - ) - - # Check the response status code, content, or any other relevant assertions - self.assertEqual(response.status_code, 200) - - # Check if the expected message tag is set - if test_name == "RegistryError" or test_name == "ContactError": - message_tag = "error" - elif test_name == "RegistrySuccess": - message_tag = "success" - else: - # Handle other cases if needed - message_tag = "info" # Change to the appropriate default - - # Check the message tag - messages = list(response.context["messages"]) - self.assertEqual(len(messages), 1) - message = messages[0] - self.assertEqual(message.tags, message_tag) - self.assertEqual(message.message.strip(), expected_message.strip()) - - def test_domain_overview_blocked_for_ineligible_user(self): - """We could easily duplicate this test for all domain management - views, but a single url test should be solid enough since all domain - management pages share the same permissions class""" - self.user.status = User.RESTRICTED - self.user.save() - home_page = self.app.get("/") - self.assertContains(home_page, "igorville.gov") - with less_console_noise(): - response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) - self.assertEqual(response.status_code, 403) - - -class TestDomainDNSSEC(TestDomainOverview): - - """MockEPPLib is already inherited.""" - - def test_dnssec_page_refreshes_enable_button(self): - """DNSSEC overview page loads when domain has no DNSSEC data - and shows a 'Enable DNSSEC' button.""" - - page = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id})) - self.assertContains(page, "Enable DNSSEC") - - def test_dnssec_page_loads_with_data_in_domain(self): - """DNSSEC overview page loads when domain has DNSSEC data - and the template contains a button to disable DNSSEC.""" - - page = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain_multdsdata.id})) - self.assertContains(page, "Disable DNSSEC") - - # Prepare the data for the POST request - post_data = { - "disable_dnssec": "Disable DNSSEC", - } - updated_page = self.client.post( - reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}), - post_data, - follow=True, - ) - - self.assertEqual(updated_page.status_code, 200) - - self.assertContains(updated_page, "Enable DNSSEC") - - def test_ds_form_loads_with_no_domain_data(self): - """DNSSEC Add DS data page loads when there is no - domain DNSSEC data and shows a button to Add new record""" - - page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dnssec_none.id})) - self.assertContains(page, "You have no DS data added") - self.assertContains(page, "Add new record") - - def test_ds_form_loads_with_ds_data(self): - """DNSSEC Add DS data page loads when there is - domain DNSSEC DS data and shows the data""" - - page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) - self.assertContains(page, "DS data record 1") - - def test_ds_data_form_modal(self): - """When user clicks on save, a modal pops up.""" - add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) - # Assert that a hidden trigger for the modal does not exist. - # This hidden trigger will pop on the page when certain condition are met: - # 1) Initial form contained DS data, 2) All data is deleted and form is - # submitted. - self.assertNotContains(add_data_page, "Trigger Disable DNSSEC Modal") - # Simulate a delete all data - form_data = {} - response = self.client.post( - reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}), - data=form_data, - ) - self.assertEqual(response.status_code, 200) # Adjust status code as needed - # Now check to see whether the JS trigger for the modal is present on the page - self.assertContains(response, "Trigger Disable DNSSEC Modal") - - def test_ds_data_form_submits(self): - """DS data form submits successfully - - Uses self.app WebTest because we need to interact with forms. - """ - add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - with less_console_noise(): # swallow log warning message - result = add_data_page.forms[0].submit() - # form submission was a post, response should be a redirect - self.assertEqual(result.status_code, 302) - self.assertEqual( - result["Location"], - reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}), - ) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - page = result.follow() - self.assertContains(page, "The DS data records for this domain have been updated.") - - def test_ds_data_form_invalid(self): - """DS data form errors with invalid data (missing required fields) - - Uses self.app WebTest because we need to interact with forms. - """ - add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # all four form fields are required, so will test with each blank - add_data_page.forms[0]["form-0-key_tag"] = "" - add_data_page.forms[0]["form-0-algorithm"] = "" - add_data_page.forms[0]["form-0-digest_type"] = "" - add_data_page.forms[0]["form-0-digest"] = "" - with less_console_noise(): # swallow logged warning message - result = add_data_page.forms[0].submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the field. - self.assertContains(result, "Key tag is required", count=2, status_code=200) - self.assertContains(result, "Algorithm is required", count=2, status_code=200) - self.assertContains(result, "Digest type is required", count=2, status_code=200) - self.assertContains(result, "Digest is required", count=2, status_code=200) - - def test_ds_data_form_invalid_keytag(self): - """DS data form errors with invalid data (key tag too large) - - Uses self.app WebTest because we need to interact with forms. - """ - add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # first two nameservers are required, so if we empty one out we should - # get a form error - add_data_page.forms[0]["form-0-key_tag"] = "65536" # > 65535 - add_data_page.forms[0]["form-0-algorithm"] = "" - add_data_page.forms[0]["form-0-digest_type"] = "" - add_data_page.forms[0]["form-0-digest"] = "" - with less_console_noise(): # swallow logged warning message - result = add_data_page.forms[0].submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the field. - self.assertContains( - result, str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE)), count=2, status_code=200 - ) - - def test_ds_data_form_invalid_digest_chars(self): - """DS data form errors with invalid data (digest contains non hexadecimal chars) - - Uses self.app WebTest because we need to interact with forms. - """ - add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # first two nameservers are required, so if we empty one out we should - # get a form error - add_data_page.forms[0]["form-0-key_tag"] = "1234" - add_data_page.forms[0]["form-0-algorithm"] = "3" - add_data_page.forms[0]["form-0-digest_type"] = "1" - add_data_page.forms[0]["form-0-digest"] = "GG1234" - with less_console_noise(): # swallow logged warning message - result = add_data_page.forms[0].submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the field. - self.assertContains( - result, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_CHARS)), count=2, status_code=200 - ) - - def test_ds_data_form_invalid_digest_sha1(self): - """DS data form errors with invalid data (digest is invalid sha-1) - - Uses self.app WebTest because we need to interact with forms. - """ - add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # first two nameservers are required, so if we empty one out we should - # get a form error - add_data_page.forms[0]["form-0-key_tag"] = "1234" - add_data_page.forms[0]["form-0-algorithm"] = "3" - add_data_page.forms[0]["form-0-digest_type"] = "1" # SHA-1 - add_data_page.forms[0]["form-0-digest"] = "A123" - with less_console_noise(): # swallow logged warning message - result = add_data_page.forms[0].submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the field. - self.assertContains( - result, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA1)), count=2, status_code=200 - ) - - def test_ds_data_form_invalid_digest_sha256(self): - """DS data form errors with invalid data (digest is invalid sha-256) - - Uses self.app WebTest because we need to interact with forms. - """ - add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # first two nameservers are required, so if we empty one out we should - # get a form error - add_data_page.forms[0]["form-0-key_tag"] = "1234" - add_data_page.forms[0]["form-0-algorithm"] = "3" - add_data_page.forms[0]["form-0-digest_type"] = "2" # SHA-256 - add_data_page.forms[0]["form-0-digest"] = "GG1234" - with less_console_noise(): # swallow logged warning message - result = add_data_page.forms[0].submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the field. - self.assertContains( - result, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA256)), count=2, status_code=200 - ) - - -class TestApplicationStatus(TestWithUser, WebTest): - def setUp(self): - super().setUp() - self.app.set_user(self.user.username) - self.client.force_login(self.user) - - def test_application_status(self): - """Checking application status page""" - application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) - application.save() - - home_page = self.app.get("/") - self.assertContains(home_page, "city.gov") - # click the "Manage" link - detail_page = home_page.click("Manage", index=0) - self.assertContains(detail_page, "city.gov") - self.assertContains(detail_page, "city1.gov") - self.assertContains(detail_page, "Chief Tester") - self.assertContains(detail_page, "testy@town.com") - self.assertContains(detail_page, "Admin Tester") - self.assertContains(detail_page, "Status:") - - def test_application_status_with_ineligible_user(self): - """Checking application status page whith a blocked user. - The user should still have access to view.""" - self.user.status = "ineligible" - self.user.save() - - application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) - application.save() - - home_page = self.app.get("/") - self.assertContains(home_page, "city.gov") - # click the "Manage" link - detail_page = home_page.click("Manage", index=0) - self.assertContains(detail_page, "city.gov") - self.assertContains(detail_page, "Chief Tester") - self.assertContains(detail_page, "testy@town.com") - self.assertContains(detail_page, "Admin Tester") - self.assertContains(detail_page, "Status:") - - def test_application_withdraw(self): - """Checking application status page""" - application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) - application.save() - - home_page = self.app.get("/") - self.assertContains(home_page, "city.gov") - # click the "Manage" link - detail_page = home_page.click("Manage", index=0) - self.assertContains(detail_page, "city.gov") - self.assertContains(detail_page, "city1.gov") - self.assertContains(detail_page, "Chief Tester") - self.assertContains(detail_page, "testy@town.com") - self.assertContains(detail_page, "Admin Tester") - self.assertContains(detail_page, "Status:") - # click the "Withdraw request" button - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - withdraw_page = detail_page.click("Withdraw request") - self.assertContains(withdraw_page, "Withdraw request for") - home_page = withdraw_page.click("Withdraw request") - # confirm that it has redirected, and the status has been updated to withdrawn - self.assertRedirects( - home_page, - "/", - status_code=302, - target_status_code=200, - fetch_redirect_response=True, - ) - home_page = self.app.get("/") - self.assertContains(home_page, "Withdrawn") - - def test_application_withdraw_no_permissions(self): - """Can't withdraw applications as a restricted user.""" - self.user.status = User.RESTRICTED - self.user.save() - application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) - application.save() - - home_page = self.app.get("/") - self.assertContains(home_page, "city.gov") - # click the "Manage" link - detail_page = home_page.click("Manage", index=0) - self.assertContains(detail_page, "city.gov") - self.assertContains(detail_page, "city1.gov") - self.assertContains(detail_page, "Chief Tester") - self.assertContains(detail_page, "testy@town.com") - self.assertContains(detail_page, "Admin Tester") - self.assertContains(detail_page, "Status:") - # Restricted user trying to withdraw results in 403 error - with less_console_noise(): - for url_name in [ - "application-withdraw-confirmation", - "application-withdrawn", - ]: - with self.subTest(url_name=url_name): - page = self.client.get(reverse(url_name, kwargs={"pk": application.pk})) - self.assertEqual(page.status_code, 403) - - def test_application_status_no_permissions(self): - """Can't access applications without being the creator.""" - application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) - other_user = User() - other_user.save() - application.creator = other_user - application.save() - - # PermissionDeniedErrors make lots of noise in test output - with less_console_noise(): - for url_name in [ - "application-status", - "application-withdraw-confirmation", - "application-withdrawn", - ]: - with self.subTest(url_name=url_name): - page = self.client.get(reverse(url_name, kwargs={"pk": application.pk})) - self.assertEqual(page.status_code, 403) - - def test_approved_application_not_in_active_requests(self): - """An approved application is not shown in the Active - Requests table on home.html.""" - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED, user=self.user) - application.save() - - home_page = self.app.get("/") - # This works in our test environment because creating - # an approved application here does not generate a - # domain object, so we do not expect to see 'city.gov' - # in either the Domains or Requests tables. - self.assertNotContains(home_page, "city.gov") diff --git a/src/registrar/tests/test_views_application.py b/src/registrar/tests/test_views_application.py new file mode 100644 index 000000000..02fe5ff76 --- /dev/null +++ b/src/registrar/tests/test_views_application.py @@ -0,0 +1,2329 @@ +from unittest import skip +from unittest.mock import Mock + +from django.conf import settings +from django.urls import reverse + +from .common import MockSESClient, completed_application # type: ignore +from django_webtest import WebTest # type: ignore +import boto3_mocking # type: ignore + +from registrar.models import ( + DomainApplication, + DraftDomain, + Domain, + DomainInformation, + Contact, + User, + Website, +) +from registrar.views.application import ApplicationWizard, Step + +from .common import less_console_noise +from .test_views import TestWithUser + +import logging + +logger = logging.getLogger(__name__) + + +class DomainApplicationTests(TestWithUser, WebTest): + + """Webtests for domain application to test filling and submitting.""" + + # Doesn't work with CSRF checking + # hypothesis is that CSRF_USE_SESSIONS is incompatible with WebTest + csrf_checks = False + + def setUp(self): + super().setUp() + self.app.set_user(self.user.username) + self.TITLES = ApplicationWizard.TITLES + + def test_application_form_intro_acknowledgement(self): + """Tests that user is presented with intro acknowledgement page""" + intro_page = self.app.get(reverse("application:")) + self.assertContains(intro_page, "You’re about to start your .gov domain request") + + def test_application_form_intro_is_skipped_when_edit_access(self): + """Tests that user is NOT presented with intro acknowledgement page when accessed through 'edit'""" + completed_application(status=DomainApplication.ApplicationStatus.STARTED, user=self.user) + home_page = self.app.get("/") + self.assertContains(home_page, "city.gov") + # click the "Edit" link + detail_page = home_page.click("Edit", index=0) + # Check that the response is a redirect + self.assertEqual(detail_page.status_code, 302) + # You can access the 'Location' header to get the redirect URL + redirect_url = detail_page.url + self.assertEqual(redirect_url, "/request/organization_type/") + + def test_application_form_empty_submit(self): + """Tests empty submit on the first page after the acknowledgement page""" + intro_page = self.app.get(reverse("application:")) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + # submitting should get back the same page if the required field is empty + result = type_page.forms[0].submit() + self.assertIn("What kind of U.S.-based government organization do you represent?", result) + + def test_application_multiple_applications_exist(self): + """Test that an info message appears when user has multiple applications already""" + # create and submit an application + application = completed_application(user=self.user) + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + application.submit() + application.save() + + # now, attempt to create another one + with less_console_noise(): + intro_page = self.app.get(reverse("application:")) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + self.assertContains(type_page, "You cannot submit this request yet") + + @boto3_mocking.patching + def test_application_form_submission(self): + """ + Can fill out the entire form and submit. + As we add additional form pages, we need to include them here to make + this test work. + + This test also looks for the long organization name on the summary page. + + This also tests for the presence of a modal trigger and the dynamic test + in the modal header on the submit page. + """ + num_pages_tested = 0 + # elections, type_of_work, tribal_government + SKIPPED_PAGES = 3 + num_pages = len(self.TITLES) - SKIPPED_PAGES + + intro_page = self.app.get(reverse("application:")) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + # ---- TYPE PAGE ---- + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = "federal" + # test next button and validate data + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_form.submit() + # should see results in db + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.organization_type, "federal") + # the post request should return a redirect to the next form in + # the application + self.assertEqual(type_result.status_code, 302) + self.assertEqual(type_result["Location"], "/request/organization_federal/") + num_pages_tested += 1 + + # ---- FEDERAL BRANCH PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + federal_page = type_result.follow() + federal_form = federal_page.forms[0] + federal_form["organization_federal-federal_type"] = "executive" + + # test next button + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + federal_result = federal_form.submit() + # validate that data from this step are being saved + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.federal_type, "executive") + # the post request should return a redirect to the next form in + # the application + self.assertEqual(federal_result.status_code, 302) + self.assertEqual(federal_result["Location"], "/request/organization_contact/") + num_pages_tested += 1 + + # ---- ORG CONTACT PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + org_contact_page = federal_result.follow() + org_contact_form = org_contact_page.forms[0] + # federal agency so we have to fill in federal_agency + org_contact_form["organization_contact-federal_agency"] = "General Services Administration" + org_contact_form["organization_contact-organization_name"] = "Testorg" + org_contact_form["organization_contact-address_line1"] = "address 1" + org_contact_form["organization_contact-address_line2"] = "address 2" + org_contact_form["organization_contact-city"] = "NYC" + org_contact_form["organization_contact-state_territory"] = "NY" + org_contact_form["organization_contact-zipcode"] = "10002" + org_contact_form["organization_contact-urbanization"] = "URB Royal Oaks" + + # test next button + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + org_contact_result = org_contact_form.submit() + # validate that data from this step are being saved + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.organization_name, "Testorg") + self.assertEqual(application.address_line1, "address 1") + self.assertEqual(application.address_line2, "address 2") + self.assertEqual(application.city, "NYC") + self.assertEqual(application.state_territory, "NY") + self.assertEqual(application.zipcode, "10002") + self.assertEqual(application.urbanization, "URB Royal Oaks") + # the post request should return a redirect to the next form in + # the application + self.assertEqual(org_contact_result.status_code, 302) + self.assertEqual(org_contact_result["Location"], "/request/authorizing_official/") + num_pages_tested += 1 + + # ---- AUTHORIZING OFFICIAL PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + ao_page = org_contact_result.follow() + ao_form = ao_page.forms[0] + ao_form["authorizing_official-first_name"] = "Testy ATO" + ao_form["authorizing_official-last_name"] = "Tester ATO" + ao_form["authorizing_official-title"] = "Chief Tester" + ao_form["authorizing_official-email"] = "testy@town.com" + + # test next button + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + ao_result = ao_form.submit() + # validate that data from this step are being saved + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.authorizing_official.first_name, "Testy ATO") + self.assertEqual(application.authorizing_official.last_name, "Tester ATO") + self.assertEqual(application.authorizing_official.title, "Chief Tester") + self.assertEqual(application.authorizing_official.email, "testy@town.com") + # the post request should return a redirect to the next form in + # the application + self.assertEqual(ao_result.status_code, 302) + self.assertEqual(ao_result["Location"], "/request/current_sites/") + num_pages_tested += 1 + + # ---- CURRENT SITES PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + current_sites_page = ao_result.follow() + current_sites_form = current_sites_page.forms[0] + current_sites_form["current_sites-0-website"] = "www.city.com" + + # test next button + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + current_sites_result = current_sites_form.submit() + # validate that data from this step are being saved + application = DomainApplication.objects.get() # there's only one + self.assertEqual( + application.current_websites.filter(website="http://www.city.com").count(), + 1, + ) + # the post request should return a redirect to the next form in + # the application + self.assertEqual(current_sites_result.status_code, 302) + self.assertEqual(current_sites_result["Location"], "/request/dotgov_domain/") + num_pages_tested += 1 + + # ---- DOTGOV DOMAIN PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + dotgov_page = current_sites_result.follow() + dotgov_form = dotgov_page.forms[0] + dotgov_form["dotgov_domain-requested_domain"] = "city" + dotgov_form["dotgov_domain-0-alternative_domain"] = "city1" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + dotgov_result = dotgov_form.submit() + # validate that data from this step are being saved + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.requested_domain.name, "city.gov") + self.assertEqual(application.alternative_domains.filter(website="city1.gov").count(), 1) + # the post request should return a redirect to the next form in + # the application + self.assertEqual(dotgov_result.status_code, 302) + self.assertEqual(dotgov_result["Location"], "/request/purpose/") + num_pages_tested += 1 + + # ---- PURPOSE PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + purpose_page = dotgov_result.follow() + purpose_form = purpose_page.forms[0] + purpose_form["purpose-purpose"] = "For all kinds of things." + + # test next button + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + purpose_result = purpose_form.submit() + # validate that data from this step are being saved + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.purpose, "For all kinds of things.") + # the post request should return a redirect to the next form in + # the application + self.assertEqual(purpose_result.status_code, 302) + self.assertEqual(purpose_result["Location"], "/request/your_contact/") + num_pages_tested += 1 + + # ---- YOUR CONTACT INFO PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + your_contact_page = purpose_result.follow() + your_contact_form = your_contact_page.forms[0] + + your_contact_form["your_contact-first_name"] = "Testy you" + your_contact_form["your_contact-last_name"] = "Tester you" + your_contact_form["your_contact-title"] = "Admin Tester" + your_contact_form["your_contact-email"] = "testy-admin@town.com" + your_contact_form["your_contact-phone"] = "(201) 555 5556" + + # test next button + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + your_contact_result = your_contact_form.submit() + # validate that data from this step are being saved + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.submitter.first_name, "Testy you") + self.assertEqual(application.submitter.last_name, "Tester you") + self.assertEqual(application.submitter.title, "Admin Tester") + self.assertEqual(application.submitter.email, "testy-admin@town.com") + self.assertEqual(application.submitter.phone, "(201) 555 5556") + # the post request should return a redirect to the next form in + # the application + self.assertEqual(your_contact_result.status_code, 302) + self.assertEqual(your_contact_result["Location"], "/request/other_contacts/") + num_pages_tested += 1 + + # ---- OTHER CONTACTS PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + other_contacts_page = your_contact_result.follow() + + # This page has 3 forms in 1. + # Let's set the yes/no radios to enable the other contacts fieldsets + other_contacts_form = other_contacts_page.forms[0] + + other_contacts_form["other_contacts-has_other_contacts"] = "True" + + other_contacts_form["other_contacts-0-first_name"] = "Testy2" + other_contacts_form["other_contacts-0-last_name"] = "Tester2" + other_contacts_form["other_contacts-0-title"] = "Another Tester" + other_contacts_form["other_contacts-0-email"] = "testy2@town.com" + other_contacts_form["other_contacts-0-phone"] = "(201) 555 5557" + + # test next button + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + other_contacts_result = other_contacts_form.submit() + # validate that data from this step are being saved + application = DomainApplication.objects.get() # there's only one + self.assertEqual( + application.other_contacts.filter( + first_name="Testy2", + last_name="Tester2", + title="Another Tester", + email="testy2@town.com", + phone="(201) 555 5557", + ).count(), + 1, + ) + # the post request should return a redirect to the next form in + # the application + self.assertEqual(other_contacts_result.status_code, 302) + self.assertEqual(other_contacts_result["Location"], "/request/anything_else/") + num_pages_tested += 1 + + # ---- ANYTHING ELSE PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + anything_else_page = other_contacts_result.follow() + anything_else_form = anything_else_page.forms[0] + + anything_else_form["anything_else-anything_else"] = "Nothing else." + + # test next button + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + anything_else_result = anything_else_form.submit() + # validate that data from this step are being saved + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.anything_else, "Nothing else.") + # the post request should return a redirect to the next form in + # the application + self.assertEqual(anything_else_result.status_code, 302) + self.assertEqual(anything_else_result["Location"], "/request/requirements/") + num_pages_tested += 1 + + # ---- REQUIREMENTS PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + requirements_page = anything_else_result.follow() + requirements_form = requirements_page.forms[0] + + requirements_form["requirements-is_policy_acknowledged"] = True + + # test next button + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + requirements_result = requirements_form.submit() + # validate that data from this step are being saved + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.is_policy_acknowledged, True) + # the post request should return a redirect to the next form in + # the application + self.assertEqual(requirements_result.status_code, 302) + self.assertEqual(requirements_result["Location"], "/request/review/") + num_pages_tested += 1 + + # ---- REVIEW AND FINSIHED PAGES ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + review_page = requirements_result.follow() + review_form = review_page.forms[0] + + # Review page contains all the previously entered data + # Let's make sure the long org name is displayed + self.assertContains(review_page, "Federal") + self.assertContains(review_page, "Executive") + self.assertContains(review_page, "Testorg") + self.assertContains(review_page, "address 1") + self.assertContains(review_page, "address 2") + self.assertContains(review_page, "NYC") + self.assertContains(review_page, "NY") + self.assertContains(review_page, "10002") + self.assertContains(review_page, "URB Royal Oaks") + self.assertContains(review_page, "Testy ATO") + self.assertContains(review_page, "Tester ATO") + self.assertContains(review_page, "Chief Tester") + self.assertContains(review_page, "testy@town.com") + self.assertContains(review_page, "city.com") + self.assertContains(review_page, "city.gov") + self.assertContains(review_page, "city1.gov") + self.assertContains(review_page, "For all kinds of things.") + self.assertContains(review_page, "Testy you") + self.assertContains(review_page, "Tester you") + self.assertContains(review_page, "Admin Tester") + self.assertContains(review_page, "testy-admin@town.com") + self.assertContains(review_page, "(201) 555-5556") + self.assertContains(review_page, "Testy2") + self.assertContains(review_page, "Tester2") + self.assertContains(review_page, "Another Tester") + self.assertContains(review_page, "testy2@town.com") + self.assertContains(review_page, "(201) 555-5557") + self.assertContains(review_page, "Nothing else.") + + # We can't test the modal itself as it relies on JS for init and triggering, + # but we can test for the existence of its trigger: + self.assertContains(review_page, "toggle-submit-domain-request") + # And the existence of the modal's data parked and ready for the js init. + # The next assert also tests for the passed requested domain context from + # the view > application_form > modal + self.assertContains(review_page, "You are about to submit a domain request for city.gov") + + # final submission results in a redirect to the "finished" URL + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + with less_console_noise(): + review_result = review_form.submit() + + self.assertEqual(review_result.status_code, 302) + self.assertEqual(review_result["Location"], "/request/finished/") + num_pages_tested += 1 + + # following this redirect is a GET request, so include the cookie + # here too. + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + with less_console_noise(): + final_result = review_result.follow() + self.assertContains(final_result, "Thanks for your domain request!") + + # check that any new pages are added to this test + self.assertEqual(num_pages, num_pages_tested) + + # This is the start of a test to check an existing application, it currently + # does not work and results in errors as noted in: + # https://github.com/cisagov/getgov/pull/728 + @skip("WIP") + def test_application_form_started_allsteps(self): + num_pages_tested = 0 + # elections, type_of_work, tribal_government + SKIPPED_PAGES = 3 + DASHBOARD_PAGE = 1 + num_pages = len(self.TITLES) - SKIPPED_PAGES + DASHBOARD_PAGE + + application = completed_application(user=self.user) + application.save() + home_page = self.app.get("/") + self.assertContains(home_page, "city.gov") + self.assertContains(home_page, "Started") + num_pages_tested += 1 + + # TODO: For some reason this click results in a new application being generated + # This appraoch is an alternatie to using get as is being done below + # + # type_page = home_page.click("Edit") + + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + url = reverse("edit-application", kwargs={"id": application.pk}) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # TODO: The following line results in a django error on middleware + response = self.client.get(url, follow=True) + self.assertContains(response, "Type of organization") + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # TODO: Step through the remaining pages + + self.assertEqual(num_pages, num_pages_tested) + + def test_application_form_conditional_federal(self): + """Federal branch question is shown for federal organizations.""" + intro_page = self.app.get(reverse("application:")) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + # ---- TYPE PAGE ---- + + # the conditional step titles shouldn't appear initially + self.assertNotContains(type_page, self.TITLES["organization_federal"]) + self.assertNotContains(type_page, self.TITLES["organization_election"]) + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = "federal" + + # set the session ID before .submit() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_form.submit() + + # the post request should return a redirect to the federal branch + # question + self.assertEqual(type_result.status_code, 302) + self.assertEqual(type_result["Location"], "/request/organization_federal/") + + # and the step label should appear in the sidebar of the resulting page + # but the step label for the elections page should not appear + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + federal_page = type_result.follow() + self.assertContains(federal_page, self.TITLES["organization_federal"]) + self.assertNotContains(federal_page, self.TITLES["organization_election"]) + + # continuing on in the flow we need to see top-level agency on the + # contact page + federal_page.forms[0]["organization_federal-federal_type"] = "executive" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + federal_result = federal_page.forms[0].submit() + # the post request should return a redirect to the contact + # question + self.assertEqual(federal_result.status_code, 302) + self.assertEqual(federal_result["Location"], "/request/organization_contact/") + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + contact_page = federal_result.follow() + self.assertContains(contact_page, "Federal agency") + + def test_application_form_conditional_elections(self): + """Election question is shown for other organizations.""" + intro_page = self.app.get(reverse("application:")) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + # ---- TYPE PAGE ---- + + # the conditional step titles shouldn't appear initially + self.assertNotContains(type_page, self.TITLES["organization_federal"]) + self.assertNotContains(type_page, self.TITLES["organization_election"]) + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = "county" + + # set the session ID before .submit() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_form.submit() + + # the post request should return a redirect to the elections question + self.assertEqual(type_result.status_code, 302) + self.assertEqual(type_result["Location"], "/request/organization_election/") + + # and the step label should appear in the sidebar of the resulting page + # but the step label for the elections page should not appear + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + election_page = type_result.follow() + self.assertContains(election_page, self.TITLES["organization_election"]) + self.assertNotContains(election_page, self.TITLES["organization_federal"]) + + # continuing on in the flow we need to NOT see top-level agency on the + # contact page + election_page.forms[0]["organization_election-is_election_board"] = "True" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + election_result = election_page.forms[0].submit() + # the post request should return a redirect to the contact + # question + self.assertEqual(election_result.status_code, 302) + self.assertEqual(election_result["Location"], "/request/organization_contact/") + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + contact_page = election_result.follow() + self.assertNotContains(contact_page, "Federal agency") + + def test_application_form_section_skipping(self): + """Can skip forward and back in sections""" + intro_page = self.app.get(reverse("application:")) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = "federal" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + federal_page = type_result.follow() + + # Now on federal type page, click back to the organization type + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + new_page = federal_page.click(str(self.TITLES["organization_type"]), index=0) + + # Should be a link to the organization_federal page + self.assertGreater( + len(new_page.html.find_all("a", href="/request/organization_federal/")), + 0, + ) + + def test_application_form_nonfederal(self): + """Non-federal organizations don't have to provide their federal agency.""" + intro_page = self.app.get(reverse("application:")) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.INTERSTATE + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + contact_page = type_result.follow() + org_contact_form = contact_page.forms[0] + + self.assertNotIn("federal_agency", org_contact_form.fields) + + # minimal fields that must be filled out + org_contact_form["organization_contact-organization_name"] = "Testorg" + org_contact_form["organization_contact-address_line1"] = "address 1" + org_contact_form["organization_contact-city"] = "NYC" + org_contact_form["organization_contact-state_territory"] = "NY" + org_contact_form["organization_contact-zipcode"] = "10002" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + contact_result = org_contact_form.submit() + + # the post request should return a redirect to the + # about your organization page if it was successful. + self.assertEqual(contact_result.status_code, 302) + self.assertEqual(contact_result["Location"], "/request/about_your_organization/") + + def test_application_about_your_organization_special(self): + """Special districts have to answer an additional question.""" + intro_page = self.app.get(reverse("application:")) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.SPECIAL_DISTRICT + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_page.forms[0].submit() + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + contact_page = type_result.follow() + + self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION]) + + def test_yes_no_form_inits_blank_for_new_application(self): + """On the Other Contacts page, the yes/no form gets initialized with nothing selected for + new applications""" + other_contacts_page = self.app.get(reverse("application:other_contacts")) + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None) + + def test_yes_no_form_inits_yes_for_application_with_other_contacts(self): + """On the Other Contacts page, the yes/no form gets initialized with YES selected if the + application has other contacts""" + # Application has other contacts by default + application = completed_application(user=self.user) + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") + + def test_yes_no_form_inits_no_for_application_with_no_other_contacts_rationale(self): + """On the Other Contacts page, the yes/no form gets initialized with NO selected if the + application has no other contacts""" + # Application has other contacts by default + application = completed_application(user=self.user, has_other_contacts=False) + application.no_other_contacts_rationale = "Hello!" + application.save() + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False") + + def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self): + """When a user submits the Other Contacts form with other contacts selected, the application's + no other contacts rationale gets deleted""" + # Application has other contacts by default + application = completed_application(user=self.user, has_other_contacts=False) + application.no_other_contacts_rationale = "Hello!" + application.save() + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False") + + other_contacts_form["other_contacts-has_other_contacts"] = "True" + + other_contacts_form["other_contacts-0-first_name"] = "Testy" + other_contacts_form["other_contacts-0-middle_name"] = "" + other_contacts_form["other_contacts-0-last_name"] = "McTesterson" + other_contacts_form["other_contacts-0-title"] = "Lord" + other_contacts_form["other_contacts-0-email"] = "testy@abc.org" + other_contacts_form["other_contacts-0-phone"] = "(201) 555-0123" + + # Submit the now empty form + other_contacts_form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Verify that the no_other_contacts_rationale we saved earlier has been removed from the database + application = DomainApplication.objects.get() + self.assertEqual( + application.other_contacts.count(), + 1, + ) + + self.assertEquals( + application.no_other_contacts_rationale, + None, + ) + + def test_submitting_no_other_contacts_rationale_deletes_other_contacts(self): + """When a user submits the Other Contacts form with no other contacts selected, the application's + other contacts get deleted for other contacts that exist and are not joined to other objects + """ + # Application has other contacts by default + application = completed_application(user=self.user) + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") + + other_contacts_form["other_contacts-has_other_contacts"] = "False" + + other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!" + + # Submit the now empty form + other_contacts_form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Verify that the no_other_contacts_rationale we saved earlier has been removed from the database + application = DomainApplication.objects.get() + self.assertEqual( + application.other_contacts.count(), + 0, + ) + + self.assertEquals( + application.no_other_contacts_rationale, + "Hello again!", + ) + + def test_submitting_no_other_contacts_rationale_removes_reference_other_contacts_when_joined(self): + """When a user submits the Other Contacts form with no other contacts selected, the application's + other contacts references get removed for other contacts that exist and are joined to other objects""" + # Populate the database with a domain application that + # has 1 "other contact" assigned to it + # We'll do it from scratch so we can reuse the other contact + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(555) 555 5555", + ) + you, _ = Contact.objects.get_or_create( + first_name="Testy you", + last_name="Tester you", + title="Admin Tester", + email="testy-admin@town.com", + phone="(555) 555 5556", + ) + other, _ = Contact.objects.get_or_create( + first_name="Testy2", + last_name="Tester2", + title="Another Tester", + email="testy2@town.com", + phone="(555) 555 5557", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + submitter=you, + creator=self.user, + status="started", + ) + application.other_contacts.add(other) + + # Now let's join the other contact to another object + domain_info = DomainInformation.objects.create(creator=self.user) + domain_info.other_contacts.set([other]) + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") + + other_contacts_form["other_contacts-has_other_contacts"] = "False" + + other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!" + + # Submit the now empty form + other_contacts_form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Verify that the no_other_contacts_rationale we saved earlier is no longer associated with the application + application = DomainApplication.objects.get() + self.assertEqual( + application.other_contacts.count(), + 0, + ) + + # Verify that the 'other' contact object still exists + domain_info = DomainInformation.objects.get() + self.assertEqual( + domain_info.other_contacts.count(), + 1, + ) + self.assertEqual( + domain_info.other_contacts.all()[0].first_name, + "Testy2", + ) + + self.assertEquals( + application.no_other_contacts_rationale, + "Hello again!", + ) + + def test_if_yes_no_form_is_no_then_no_other_contacts_required(self): + """Applicants with no other contacts have to give a reason.""" + other_contacts_page = self.app.get(reverse("application:other_contacts")) + other_contacts_form = other_contacts_page.forms[0] + other_contacts_form["other_contacts-has_other_contacts"] = "False" + response = other_contacts_page.forms[0].submit() + + # The textarea for no other contacts returns this error message + # Assert that it is returned, ie the no other contacts form is required + self.assertContains(response, "Rationale for no other employees is required.") + + # The first name field for other contacts returns this error message + # Assert that it is not returned, ie the contacts form is not required + self.assertNotContains(response, "Enter the first name / given name of this contact.") + + def test_if_yes_no_form_is_yes_then_other_contacts_required(self): + """Applicants with other contacts do not have to give a reason.""" + other_contacts_page = self.app.get(reverse("application:other_contacts")) + other_contacts_form = other_contacts_page.forms[0] + other_contacts_form["other_contacts-has_other_contacts"] = "True" + response = other_contacts_page.forms[0].submit() + + # The textarea for no other contacts returns this error message + # Assert that it is not returned, ie the no other contacts form is not required + self.assertNotContains(response, "Rationale for no other employees is required.") + + # The first name field for other contacts returns this error message + # Assert that it is returned, ie the contacts form is required + self.assertContains(response, "Enter the first name / given name of this contact.") + + def test_delete_other_contact(self): + """Other contacts can be deleted after being saved to database. + + This formset uses the DJANGO DELETE widget. We'll test that by setting 2 contacts on an application, + loading the form and marking one contact up for deletion.""" + # Populate the database with a domain application that + # has 2 "other contact" assigned to it + # We'll do it from scratch so we can reuse the other contact + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + you, _ = Contact.objects.get_or_create( + first_name="Testy you", + last_name="Tester you", + title="Admin Tester", + email="testy-admin@town.com", + phone="(201) 555 5556", + ) + other, _ = Contact.objects.get_or_create( + first_name="Testy2", + last_name="Tester2", + title="Another Tester", + email="testy2@town.com", + phone="(201) 555 5557", + ) + other2, _ = Contact.objects.get_or_create( + first_name="Testy3", + last_name="Tester3", + title="Another Tester", + email="testy3@town.com", + phone="(201) 555 5557", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + submitter=you, + creator=self.user, + status="started", + ) + application.other_contacts.add(other) + application.other_contacts.add(other2) + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + + # Minimal check to ensure the form is loaded with both other contacts + self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2") + self.assertEqual(other_contacts_form["other_contacts-1-first_name"].value, "Testy3") + + # Mark the first dude for deletion + other_contacts_form.set("other_contacts-0-DELETE", "on") + + # Submit the form + other_contacts_form.submit() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Verify that the first dude was deleted + application = DomainApplication.objects.get() + self.assertEqual(application.other_contacts.count(), 1) + self.assertEqual(application.other_contacts.first().first_name, "Testy3") + + def test_delete_other_contact_does_not_allow_zero_contacts(self): + """Delete Other Contact does not allow submission with zero contacts.""" + # Populate the database with a domain application that + # has 1 "other contact" assigned to it + # We'll do it from scratch so we can reuse the other contact + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + you, _ = Contact.objects.get_or_create( + first_name="Testy you", + last_name="Tester you", + title="Admin Tester", + email="testy-admin@town.com", + phone="(201) 555 5556", + ) + other, _ = Contact.objects.get_or_create( + first_name="Testy2", + last_name="Tester2", + title="Another Tester", + email="testy2@town.com", + phone="(201) 555 5557", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + submitter=you, + creator=self.user, + status="started", + ) + application.other_contacts.add(other) + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + + # Minimal check to ensure the form is loaded + self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2") + + # Mark the first dude for deletion + other_contacts_form.set("other_contacts-0-DELETE", "on") + + # Submit the form + other_contacts_form.submit() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Verify that the contact was not deleted + application = DomainApplication.objects.get() + self.assertEqual(application.other_contacts.count(), 1) + self.assertEqual(application.other_contacts.first().first_name, "Testy2") + + def test_delete_other_contact_sets_visible_empty_form_as_required_after_failed_submit(self): + """When you: + 1. add an empty contact, + 2. delete existing contacts, + 3. then submit, + The forms on page reload shows all the required fields and their errors.""" + + # Populate the database with a domain application that + # has 1 "other contact" assigned to it + # We'll do it from scratch so we can reuse the other contact + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + you, _ = Contact.objects.get_or_create( + first_name="Testy you", + last_name="Tester you", + title="Admin Tester", + email="testy-admin@town.com", + phone="(201) 555 5556", + ) + other, _ = Contact.objects.get_or_create( + first_name="Testy2", + last_name="Tester2", + title="Another Tester", + email="testy2@town.com", + phone="(201) 555 5557", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + submitter=you, + creator=self.user, + status="started", + ) + application.other_contacts.add(other) + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + + # Minimal check to ensure the form is loaded + self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2") + + # Set total forms to 2 indicating an additional formset was added. + # Submit no data though for the second formset. + # Set the first formset to be deleted. + other_contacts_form["other_contacts-TOTAL_FORMS"] = "2" + other_contacts_form.set("other_contacts-0-DELETE", "on") + + response = other_contacts_form.submit() + + # Assert that the response presents errors to the user, including to + # Enter the first name ... + self.assertContains(response, "Enter the first name / given name of this contact.") + + def test_edit_other_contact_in_place(self): + """When you: + 1. edit an existing contact which is not joined to another model, + 2. then submit, + The application is linked to the existing contact, and the existing contact updated.""" + + # Populate the database with a domain application that + # has 1 "other contact" assigned to it + # We'll do it from scratch + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + you, _ = Contact.objects.get_or_create( + first_name="Testy you", + last_name="Tester you", + title="Admin Tester", + email="testy-admin@town.com", + phone="(201) 555 5556", + ) + other, _ = Contact.objects.get_or_create( + first_name="Testy2", + last_name="Tester2", + title="Another Tester", + email="testy2@town.com", + phone="(201) 555 5557", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + submitter=you, + creator=self.user, + status="started", + ) + application.other_contacts.add(other) + + # other_contact_pk is the initial pk of the other contact. set it before update + # to be able to verify after update that the same contact object is in place + other_contact_pk = other.id + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + + # Minimal check to ensure the form is loaded + self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2") + + # update the first name of the contact + other_contacts_form["other_contacts-0-first_name"] = "Testy3" + + # Submit the updated form + other_contacts_form.submit() + + application.refresh_from_db() + + # assert that the Other Contact is updated "in place" + other_contact = application.other_contacts.all()[0] + self.assertEquals(other_contact_pk, other_contact.id) + self.assertEquals("Testy3", other_contact.first_name) + + def test_edit_other_contact_creates_new(self): + """When you: + 1. edit an existing contact which IS joined to another model, + 2. then submit, + The application is linked to a new contact, and the new contact is updated.""" + + # Populate the database with a domain application that + # has 1 "other contact" assigned to it, the other contact is also + # the authorizing official initially + # We'll do it from scratch + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + you, _ = Contact.objects.get_or_create( + first_name="Testy you", + last_name="Tester you", + title="Admin Tester", + email="testy-admin@town.com", + phone="(201) 555 5556", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + submitter=you, + creator=self.user, + status="started", + ) + application.other_contacts.add(ao) + + # other_contact_pk is the initial pk of the other contact. set it before update + # to be able to verify after update that the ao contact is still in place + # and not updated, and that the new contact has a new id + other_contact_pk = ao.id + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + + # Minimal check to ensure the form is loaded + self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy") + + # update the first name of the contact + other_contacts_form["other_contacts-0-first_name"] = "Testy2" + + # Submit the updated form + other_contacts_form.submit() + + application.refresh_from_db() + + # assert that other contact info is updated, and that a new Contact + # is created for the other contact + other_contact = application.other_contacts.all()[0] + self.assertNotEquals(other_contact_pk, other_contact.id) + self.assertEquals("Testy2", other_contact.first_name) + # assert that the authorizing official is not updated + authorizing_official = application.authorizing_official + self.assertEquals("Testy", authorizing_official.first_name) + + def test_edit_authorizing_official_in_place(self): + """When you: + 1. edit an authorizing official which is not joined to another model, + 2. then submit, + The application is linked to the existing ao, and the ao updated.""" + + # Populate the database with a domain application that + # has an authorizing_official (ao) + # We'll do it from scratch + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + creator=self.user, + status="started", + ) + + # ao_pk is the initial pk of the Authorizing Official. set it before update + # to be able to verify after update that the same Contact object is in place + ao_pk = ao.id + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + ao_page = self.app.get(reverse("application:authorizing_official")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + ao_form = ao_page.forms[0] + + # Minimal check to ensure the form is loaded + self.assertEqual(ao_form["authorizing_official-first_name"].value, "Testy") + + # update the first name of the contact + ao_form["authorizing_official-first_name"] = "Testy2" + + # Submit the updated form + ao_form.submit() + + application.refresh_from_db() + + # assert AO is updated "in place" + updated_ao = application.authorizing_official + self.assertEquals(ao_pk, updated_ao.id) + self.assertEquals("Testy2", updated_ao.first_name) + + def test_edit_authorizing_official_creates_new(self): + """When you: + 1. edit an existing authorizing official which IS joined to another model, + 2. then submit, + The application is linked to a new Contact, and the new Contact is updated.""" + + # Populate the database with a domain application that + # has authorizing official assigned to it, the authorizing offical is also + # an other contact initially + # We'll do it from scratch + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + creator=self.user, + status="started", + ) + application.other_contacts.add(ao) + + # ao_pk is the initial pk of the authorizing official. set it before update + # to be able to verify after update that the other contact is still in place + # and not updated, and that the new ao has a new id + ao_pk = ao.id + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + ao_page = self.app.get(reverse("application:authorizing_official")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + ao_form = ao_page.forms[0] + + # Minimal check to ensure the form is loaded + self.assertEqual(ao_form["authorizing_official-first_name"].value, "Testy") + + # update the first name of the contact + ao_form["authorizing_official-first_name"] = "Testy2" + + # Submit the updated form + ao_form.submit() + + application.refresh_from_db() + + # assert that the other contact is not updated + other_contacts = application.other_contacts.all() + other_contact = other_contacts[0] + self.assertEquals(ao_pk, other_contact.id) + self.assertEquals("Testy", other_contact.first_name) + # assert that the authorizing official is updated + authorizing_official = application.authorizing_official + self.assertEquals("Testy2", authorizing_official.first_name) + + def test_edit_submitter_in_place(self): + """When you: + 1. edit a submitter (your contact) which is not joined to another model, + 2. then submit, + The application is linked to the existing submitter, and the submitter updated.""" + + # Populate the database with a domain application that + # has a submitter + # We'll do it from scratch + you, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + submitter=you, + creator=self.user, + status="started", + ) + + # submitter_pk is the initial pk of the submitter. set it before update + # to be able to verify after update that the same contact object is in place + submitter_pk = you.id + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + your_contact_page = self.app.get(reverse("application:your_contact")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + your_contact_form = your_contact_page.forms[0] + + # Minimal check to ensure the form is loaded + self.assertEqual(your_contact_form["your_contact-first_name"].value, "Testy") + + # update the first name of the contact + your_contact_form["your_contact-first_name"] = "Testy2" + + # Submit the updated form + your_contact_form.submit() + + application.refresh_from_db() + + updated_submitter = application.submitter + self.assertEquals(submitter_pk, updated_submitter.id) + self.assertEquals("Testy2", updated_submitter.first_name) + + def test_edit_submitter_creates_new(self): + """When you: + 1. edit an existing your contact which IS joined to another model, + 2. then submit, + The application is linked to a new Contact, and the new Contact is updated.""" + + # Populate the database with a domain application that + # has submitter assigned to it, the submitter is also + # an other contact initially + # We'll do it from scratch + submitter, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + submitter=submitter, + creator=self.user, + status="started", + ) + application.other_contacts.add(submitter) + + # submitter_pk is the initial pk of the your contact. set it before update + # to be able to verify after update that the other contact is still in place + # and not updated, and that the new submitter has a new id + submitter_pk = submitter.id + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + your_contact_page = self.app.get(reverse("application:your_contact")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + your_contact_form = your_contact_page.forms[0] + + # Minimal check to ensure the form is loaded + self.assertEqual(your_contact_form["your_contact-first_name"].value, "Testy") + + # update the first name of the contact + your_contact_form["your_contact-first_name"] = "Testy2" + + # Submit the updated form + your_contact_form.submit() + + application.refresh_from_db() + + # assert that the other contact is not updated + other_contacts = application.other_contacts.all() + other_contact = other_contacts[0] + self.assertEquals(submitter_pk, other_contact.id) + self.assertEquals("Testy", other_contact.first_name) + # assert that the submitter is updated + submitter = application.submitter + self.assertEquals("Testy2", submitter.first_name) + + def test_application_about_your_organiztion_interstate(self): + """Special districts have to answer an additional question.""" + intro_page = self.app.get(reverse("application:")) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.INTERSTATE + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_form.submit() + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + contact_page = type_result.follow() + + self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION]) + + def test_application_tribal_government(self): + """Tribal organizations have to answer an additional question.""" + intro_page = self.app.get(reverse("application:")) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.TRIBAL + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_form.submit() + # the tribal government page comes immediately afterwards + self.assertIn("/tribal_government", type_result.headers["Location"]) + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + tribal_government_page = type_result.follow() + + # and the step is on the sidebar list. + self.assertContains(tribal_government_page, self.TITLES[Step.TRIBAL_GOVERNMENT]) + + def test_application_ao_dynamic_text(self): + intro_page = self.app.get(reverse("application:")) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + # ---- TYPE PAGE ---- + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = "federal" + + # test next button + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_form.submit() + + # ---- FEDERAL BRANCH PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + federal_page = type_result.follow() + federal_form = federal_page.forms[0] + federal_form["organization_federal-federal_type"] = "executive" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + federal_result = federal_form.submit() + + # ---- ORG CONTACT PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + org_contact_page = federal_result.follow() + org_contact_form = org_contact_page.forms[0] + # federal agency so we have to fill in federal_agency + org_contact_form["organization_contact-federal_agency"] = "General Services Administration" + org_contact_form["organization_contact-organization_name"] = "Testorg" + org_contact_form["organization_contact-address_line1"] = "address 1" + org_contact_form["organization_contact-address_line2"] = "address 2" + org_contact_form["organization_contact-city"] = "NYC" + org_contact_form["organization_contact-state_territory"] = "NY" + org_contact_form["organization_contact-zipcode"] = "10002" + org_contact_form["organization_contact-urbanization"] = "URB Royal Oaks" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + org_contact_result = org_contact_form.submit() + + # ---- AO CONTACT PAGE ---- + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + ao_page = org_contact_result.follow() + self.assertContains(ao_page, "Executive branch federal agencies") + + # Go back to organization type page and change type + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + ao_page.click(str(self.TITLES["organization_type"]), index=0) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_form["organization_type-organization_type"] = "city" + type_result = type_form.submit() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + election_page = type_result.follow() + + # Go back to AO page and test the dynamic text changed + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + ao_page = election_page.click(str(self.TITLES["authorizing_official"]), index=0) + self.assertContains(ao_page, "Domain requests from cities") + + def test_application_dotgov_domain_dynamic_text(self): + intro_page = self.app.get(reverse("application:")) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + # ---- TYPE PAGE ---- + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = "federal" + + # test next button + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_form.submit() + + # ---- FEDERAL BRANCH PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + federal_page = type_result.follow() + federal_form = federal_page.forms[0] + federal_form["organization_federal-federal_type"] = "executive" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + federal_result = federal_form.submit() + + # ---- ORG CONTACT PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + org_contact_page = federal_result.follow() + org_contact_form = org_contact_page.forms[0] + # federal agency so we have to fill in federal_agency + org_contact_form["organization_contact-federal_agency"] = "General Services Administration" + org_contact_form["organization_contact-organization_name"] = "Testorg" + org_contact_form["organization_contact-address_line1"] = "address 1" + org_contact_form["organization_contact-address_line2"] = "address 2" + org_contact_form["organization_contact-city"] = "NYC" + org_contact_form["organization_contact-state_territory"] = "NY" + org_contact_form["organization_contact-zipcode"] = "10002" + org_contact_form["organization_contact-urbanization"] = "URB Royal Oaks" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + org_contact_result = org_contact_form.submit() + + # ---- AO CONTACT PAGE ---- + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + ao_page = org_contact_result.follow() + + # ---- AUTHORIZING OFFICIAL PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + ao_page = org_contact_result.follow() + ao_form = ao_page.forms[0] + ao_form["authorizing_official-first_name"] = "Testy ATO" + ao_form["authorizing_official-last_name"] = "Tester ATO" + ao_form["authorizing_official-title"] = "Chief Tester" + ao_form["authorizing_official-email"] = "testy@town.com" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + ao_result = ao_form.submit() + + # ---- CURRENT SITES PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + current_sites_page = ao_result.follow() + current_sites_form = current_sites_page.forms[0] + current_sites_form["current_sites-0-website"] = "www.city.com" + + # test saving the page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + current_sites_result = current_sites_form.submit() + + # ---- DOTGOV DOMAIN PAGE ---- + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + dotgov_page = current_sites_result.follow() + + self.assertContains(dotgov_page, "medicare.gov") + + # Go back to organization type page and change type + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + dotgov_page.click(str(self.TITLES["organization_type"]), index=0) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_form["organization_type-organization_type"] = "city" + type_result = type_form.submit() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + election_page = type_result.follow() + + # Go back to dotgov domain page to test the dynamic text changed + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + dotgov_page = election_page.click(str(self.TITLES["dotgov_domain"]), index=0) + self.assertContains(dotgov_page, "CityofEudoraKS.gov") + self.assertNotContains(dotgov_page, "medicare.gov") + + def test_application_formsets(self): + """Users are able to add more than one of some fields.""" + current_sites_page = self.app.get(reverse("application:current_sites")) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + # fill in the form field + current_sites_form = current_sites_page.forms[0] + self.assertIn("current_sites-0-website", current_sites_form.fields) + self.assertNotIn("current_sites-1-website", current_sites_form.fields) + current_sites_form["current_sites-0-website"] = "https://example.com" + + # click "Add another" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + current_sites_result = current_sites_form.submit("submit_button", value="save") + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + current_sites_form = current_sites_result.follow().forms[0] + + # verify that there are two form fields + value = current_sites_form["current_sites-0-website"].value + self.assertEqual(value, "https://example.com") + self.assertIn("current_sites-1-website", current_sites_form.fields) + # and it is correctly referenced in the ManyToOne relationship + application = DomainApplication.objects.get() # there's only one + self.assertEqual( + application.current_websites.filter(website="https://example.com").count(), + 1, + ) + + @skip("WIP") + def test_application_edit_restore(self): + """ + Test that a previously saved application is available at the /edit endpoint. + """ + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(555) 555 5555", + ) + domain, _ = Domain.objects.get_or_create(name="city.gov") + alt, _ = Website.objects.get_or_create(website="city1.gov") + current, _ = Website.objects.get_or_create(website="city.com") + you, _ = Contact.objects.get_or_create( + first_name="Testy you", + last_name="Tester you", + title="Admin Tester", + email="testy-admin@town.com", + phone="(555) 555 5556", + ) + other, _ = Contact.objects.get_or_create( + first_name="Testy2", + last_name="Tester2", + title="Another Tester", + email="testy2@town.com", + phone="(555) 555 5557", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + requested_domain=domain, + submitter=you, + creator=self.user, + ) + application.other_contacts.add(other) + application.current_websites.add(current) + application.alternative_domains.add(alt) + + # prime the form by visiting /edit + url = reverse("edit-application", kwargs={"id": application.pk}) + response = self.client.get(url) + + # TODO: this is a sketch of each page in the wizard which needs to be tested + # Django does not have tools sufficient for real end to end integration testing + # (for example, USWDS moves radio buttons off screen and replaces them with + # CSS styled "fakes" -- Django cannot determine if those are visually correct) + # -- the best that can/should be done here is to ensure the correct values + # are being passed to the templating engine + + url = reverse("application:organization_type") + response = self.client.get(url, follow=True) + self.assertContains(response, "") + # choices = response.context['wizard']['form']['organization_type'].subwidgets + # radio = [ x for x in choices if x.data["value"] == "federal" ][0] + # checked = radio.data["selected"] + # self.assertTrue(checked) + + # url = reverse("application:organization_federal") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:organization_contact") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:authorizing_official") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:current_sites") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:dotgov_domain") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:purpose") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:your_contact") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:other_contacts") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:other_contacts") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:security_email") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:anything_else") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:requirements") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + def test_long_org_name_in_application(self): + """ + Make sure the long name is displaying in the application form, + org step + """ + intro_page = self.app.get(reverse("application:")) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + + self.assertContains(type_page, "Federal: an agency of the U.S. government") + + def test_submit_modal_no_domain_text_fallback(self): + """When user clicks on submit your domain request and the requested domain + is null (possible through url direct access to the review page), present + fallback copy in the modal's header. + + NOTE: This may be a moot point if we implement a more solid pattern in the + future, like not a submit action at all on the review page.""" + + review_page = self.app.get(reverse("application:review")) + self.assertContains(review_page, "toggle-submit-domain-request") + self.assertContains(review_page, "You are about to submit an incomplete request") + + +class DomainApplicationTestDifferentStatuses(TestWithUser, WebTest): + def setUp(self): + super().setUp() + self.app.set_user(self.user.username) + self.client.force_login(self.user) + + def test_application_status(self): + """Checking application status page""" + application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) + application.save() + + home_page = self.app.get("/") + self.assertContains(home_page, "city.gov") + # click the "Manage" link + detail_page = home_page.click("Manage", index=0) + self.assertContains(detail_page, "city.gov") + self.assertContains(detail_page, "city1.gov") + self.assertContains(detail_page, "Chief Tester") + self.assertContains(detail_page, "testy@town.com") + self.assertContains(detail_page, "Admin Tester") + self.assertContains(detail_page, "Status:") + + def test_application_status_with_ineligible_user(self): + """Checking application status page whith a blocked user. + The user should still have access to view.""" + self.user.status = "ineligible" + self.user.save() + + application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) + application.save() + + home_page = self.app.get("/") + self.assertContains(home_page, "city.gov") + # click the "Manage" link + detail_page = home_page.click("Manage", index=0) + self.assertContains(detail_page, "city.gov") + self.assertContains(detail_page, "Chief Tester") + self.assertContains(detail_page, "testy@town.com") + self.assertContains(detail_page, "Admin Tester") + self.assertContains(detail_page, "Status:") + + def test_application_withdraw(self): + """Checking application status page""" + application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) + application.save() + + home_page = self.app.get("/") + self.assertContains(home_page, "city.gov") + # click the "Manage" link + detail_page = home_page.click("Manage", index=0) + self.assertContains(detail_page, "city.gov") + self.assertContains(detail_page, "city1.gov") + self.assertContains(detail_page, "Chief Tester") + self.assertContains(detail_page, "testy@town.com") + self.assertContains(detail_page, "Admin Tester") + self.assertContains(detail_page, "Status:") + # click the "Withdraw request" button + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + withdraw_page = detail_page.click("Withdraw request") + self.assertContains(withdraw_page, "Withdraw request for") + home_page = withdraw_page.click("Withdraw request") + # confirm that it has redirected, and the status has been updated to withdrawn + self.assertRedirects( + home_page, + "/", + status_code=302, + target_status_code=200, + fetch_redirect_response=True, + ) + home_page = self.app.get("/") + self.assertContains(home_page, "Withdrawn") + + def test_application_withdraw_no_permissions(self): + """Can't withdraw applications as a restricted user.""" + self.user.status = User.RESTRICTED + self.user.save() + application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) + application.save() + + home_page = self.app.get("/") + self.assertContains(home_page, "city.gov") + # click the "Manage" link + detail_page = home_page.click("Manage", index=0) + self.assertContains(detail_page, "city.gov") + self.assertContains(detail_page, "city1.gov") + self.assertContains(detail_page, "Chief Tester") + self.assertContains(detail_page, "testy@town.com") + self.assertContains(detail_page, "Admin Tester") + self.assertContains(detail_page, "Status:") + # Restricted user trying to withdraw results in 403 error + with less_console_noise(): + for url_name in [ + "application-withdraw-confirmation", + "application-withdrawn", + ]: + with self.subTest(url_name=url_name): + page = self.client.get(reverse(url_name, kwargs={"pk": application.pk})) + self.assertEqual(page.status_code, 403) + + def test_application_status_no_permissions(self): + """Can't access applications without being the creator.""" + application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) + other_user = User() + other_user.save() + application.creator = other_user + application.save() + + # PermissionDeniedErrors make lots of noise in test output + with less_console_noise(): + for url_name in [ + "application-status", + "application-withdraw-confirmation", + "application-withdrawn", + ]: + with self.subTest(url_name=url_name): + page = self.client.get(reverse(url_name, kwargs={"pk": application.pk})) + self.assertEqual(page.status_code, 403) + + def test_approved_application_not_in_active_requests(self): + """An approved application is not shown in the Active + Requests table on home.html.""" + application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED, user=self.user) + application.save() + + home_page = self.app.get("/") + # This works in our test environment because creating + # an approved application here does not generate a + # domain object, so we do not expect to see 'city.gov' + # in either the Domains or Requests tables. + self.assertNotContains(home_page, "city.gov") + + +class TestWizardUnlockingSteps(TestWithUser, WebTest): + def setUp(self): + super().setUp() + self.app.set_user(self.user.username) + self.wizard = ApplicationWizard() + # Mock the request object, its user, and session attributes appropriately + self.wizard.request = Mock(user=self.user, session={}) + + def tearDown(self): + super().tearDown() + + def test_unlocked_steps_empty_application(self): + """Test when all fields in the application are empty.""" + unlocked_steps = self.wizard.db_check_for_unlocking_steps() + expected_dict = [] + self.assertEqual(unlocked_steps, expected_dict) + + def test_unlocked_steps_full_application(self): + """Test when all fields in the application are filled.""" + + completed_application(status=DomainApplication.ApplicationStatus.STARTED, user=self.user) + # Make a request to the home page + home_page = self.app.get("/") + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Assert that the response contains "city.gov" + self.assertContains(home_page, "city.gov") + + # Click the "Edit" link + response = home_page.click("Edit", index=0) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Check if the response is a redirect + if response.status_code == 302: + # Follow the redirect manually + try: + detail_page = response.follow() + + self.wizard.get_context_data() + except Exception as err: + # Handle any potential errors while following the redirect + self.fail(f"Error following the redirect {err}") + + # Now 'detail_page' contains the response after following the redirect + self.assertEqual(detail_page.status_code, 200) + + # 10 unlocked steps, one active step, the review step will have link_usa but not check_circle + self.assertContains(detail_page, "#check_circle", count=10) + # Type of organization + self.assertContains(detail_page, "usa-current", count=1) + self.assertContains(detail_page, "link_usa-checked", count=11) + + else: + self.fail(f"Expected a redirect, but got a different response: {response}") + + def test_unlocked_steps_partial_application(self): + """Test when some fields in the application are filled.""" + + # Create the site and contacts to delete (orphaned) + contact = Contact.objects.create( + first_name="Henry", + last_name="Mcfakerson", + ) + # Create two non-orphaned contacts + contact_2 = Contact.objects.create( + first_name="Saturn", + last_name="Mars", + ) + + # Attach a user object to a contact (should not be deleted) + contact_user, _ = Contact.objects.get_or_create(user=self.user) + + site = DraftDomain.objects.create(name="igorville.gov") + application = DomainApplication.objects.create( + creator=self.user, + requested_domain=site, + status=DomainApplication.ApplicationStatus.WITHDRAWN, + authorizing_official=contact, + submitter=contact_user, + ) + application.other_contacts.set([contact_2]) + + # Make a request to the home page + home_page = self.app.get("/") + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Assert that the response contains "city.gov" + self.assertContains(home_page, "igorville.gov") + + # Click the "Edit" link + response = home_page.click("Edit", index=0) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Check if the response is a redirect + if response.status_code == 302: + # Follow the redirect manually + try: + detail_page = response.follow() + + self.wizard.get_context_data() + except Exception as err: + # Handle any potential errors while following the redirect + self.fail(f"Error following the redirect {err}") + + # Now 'detail_page' contains the response after following the redirect + self.assertEqual(detail_page.status_code, 200) + + # 5 unlocked steps (ao, domain, submitter, other contacts, and current sites + # which unlocks if domain exists), one active step, the review step is locked + self.assertContains(detail_page, "#check_circle", count=5) + # Type of organization + self.assertContains(detail_page, "usa-current", count=1) + self.assertContains(detail_page, "link_usa-checked", count=5) + + else: + self.fail(f"Expected a redirect, but got a different response: {response}") diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py new file mode 100644 index 000000000..c9422e700 --- /dev/null +++ b/src/registrar/tests/test_views_domain.py @@ -0,0 +1,1436 @@ +from unittest import skip +from unittest.mock import MagicMock, ANY, patch + +from django.conf import settings +from django.urls import reverse +from django.contrib.auth import get_user_model + +from .common import MockSESClient, create_user # type: ignore +from django_webtest import WebTest # type: ignore +import boto3_mocking # type: ignore + +from registrar.utility.errors import ( + NameserverError, + NameserverErrorCodes, + SecurityEmailError, + SecurityEmailErrorCodes, + GenericError, + GenericErrorCodes, + DsDataError, + DsDataErrorCodes, +) + +from registrar.models import ( + DomainApplication, + Domain, + DomainInformation, + DomainInvitation, + Contact, + PublicContact, + Host, + HostIP, + UserDomainRole, + User, +) +from datetime import date, datetime, timedelta +from django.utils import timezone + +from .common import less_console_noise +from .test_views import TestWithUser + +import logging + +logger = logging.getLogger(__name__) + + +class TestWithDomainPermissions(TestWithUser): + def setUp(self): + super().setUp() + self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") + self.domain_with_ip, _ = Domain.objects.get_or_create(name="nameserverwithip.gov") + self.domain_just_nameserver, _ = Domain.objects.get_or_create(name="justnameserver.com") + self.domain_no_information, _ = Domain.objects.get_or_create(name="noinformation.gov") + self.domain_on_hold, _ = Domain.objects.get_or_create( + name="on-hold.gov", + state=Domain.State.ON_HOLD, + expiration_date=timezone.make_aware( + datetime.combine(date.today() + timedelta(days=1), datetime.min.time()) + ), + ) + self.domain_deleted, _ = Domain.objects.get_or_create( + name="deleted.gov", + state=Domain.State.DELETED, + expiration_date=timezone.make_aware( + datetime.combine(date.today() + timedelta(days=1), datetime.min.time()) + ), + ) + + self.domain_dsdata, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") + self.domain_multdsdata, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov") + # We could simply use domain (igorville) but this will be more readable in tests + # that inherit this setUp + self.domain_dnssec_none, _ = Domain.objects.get_or_create(name="dnssec-none.gov") + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dsdata) + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_multdsdata) + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dnssec_none) + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_ip) + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_just_nameserver) + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_on_hold) + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_deleted) + + self.role, _ = UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER + ) + + UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.MANAGER + ) + UserDomainRole.objects.get_or_create( + user=self.user, + domain=self.domain_multdsdata, + role=UserDomainRole.Roles.MANAGER, + ) + UserDomainRole.objects.get_or_create( + user=self.user, + domain=self.domain_dnssec_none, + role=UserDomainRole.Roles.MANAGER, + ) + UserDomainRole.objects.get_or_create( + user=self.user, + domain=self.domain_with_ip, + role=UserDomainRole.Roles.MANAGER, + ) + UserDomainRole.objects.get_or_create( + user=self.user, + domain=self.domain_just_nameserver, + role=UserDomainRole.Roles.MANAGER, + ) + UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain_on_hold, role=UserDomainRole.Roles.MANAGER + ) + UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain_deleted, role=UserDomainRole.Roles.MANAGER + ) + + def tearDown(self): + try: + UserDomainRole.objects.all().delete() + if hasattr(self.domain, "contacts"): + self.domain.contacts.all().delete() + DomainApplication.objects.all().delete() + DomainInformation.objects.all().delete() + PublicContact.objects.all().delete() + HostIP.objects.all().delete() + Host.objects.all().delete() + Domain.objects.all().delete() + UserDomainRole.objects.all().delete() + except ValueError: # pass if already deleted + pass + super().tearDown() + + +class TestDomainPermissions(TestWithDomainPermissions): + def test_not_logged_in(self): + """Not logged in gets a redirect to Login.""" + for view_name in [ + "domain", + "domain-users", + "domain-users-add", + "domain-dns-nameservers", + "domain-org-name-address", + "domain-authorizing-official", + "domain-your-contact-information", + "domain-security-email", + ]: + with self.subTest(view_name=view_name): + response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id})) + self.assertEqual(response.status_code, 302) + + def test_no_domain_role(self): + """Logged in but no role gets 403 Forbidden.""" + self.client.force_login(self.user) + self.role.delete() # user no longer has a role on this domain + + for view_name in [ + "domain", + "domain-users", + "domain-users-add", + "domain-dns-nameservers", + "domain-org-name-address", + "domain-authorizing-official", + "domain-your-contact-information", + "domain-security-email", + ]: + with self.subTest(view_name=view_name): + with less_console_noise(): + response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id})) + self.assertEqual(response.status_code, 403) + + def test_domain_pages_blocked_for_on_hold_and_deleted(self): + """Test that the domain pages are blocked for on hold and deleted domains""" + + self.client.force_login(self.user) + for view_name in [ + "domain-users", + "domain-users-add", + "domain-dns", + "domain-dns-nameservers", + "domain-dns-dnssec", + "domain-dns-dnssec-dsdata", + "domain-org-name-address", + "domain-authorizing-official", + "domain-your-contact-information", + "domain-security-email", + ]: + for domain in [ + self.domain_on_hold, + self.domain_deleted, + ]: + with self.subTest(view_name=view_name, domain=domain): + with less_console_noise(): + response = self.client.get(reverse(view_name, kwargs={"pk": domain.id})) + self.assertEqual(response.status_code, 403) + + +class TestDomainOverview(TestWithDomainPermissions, WebTest): + def setUp(self): + super().setUp() + self.app.set_user(self.user.username) + self.client.force_login(self.user) + + +class TestDomainDetail(TestDomainOverview): + @skip("Assertion broke for no reason, why? Need to fix") + def test_domain_detail_link_works(self): + home_page = self.app.get("/") + logger.info(f"This is the value of home_page: {home_page}") + self.assertContains(home_page, "igorville.gov") + # click the "Edit" link + detail_page = home_page.click("Manage", index=0) + self.assertContains(detail_page, "igorville.gov") + self.assertContains(detail_page, "Status") + + def test_unknown_domain_does_not_show_as_expired_on_homepage(self): + """An UNKNOWN domain does not show as expired on the homepage. + It shows as 'DNS needed'""" + # At the time of this test's writing, there are 6 UNKNOWN domains inherited + # from constructors. Let's reset. + with less_console_noise(): + Domain.objects.all().delete() + UserDomainRole.objects.all().delete() + self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") + home_page = self.app.get("/") + self.assertNotContains(home_page, "igorville.gov") + self.role, _ = UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER + ) + home_page = self.app.get("/") + self.assertContains(home_page, "igorville.gov") + igorville = Domain.objects.get(name="igorville.gov") + self.assertEquals(igorville.state, Domain.State.UNKNOWN) + self.assertNotContains(home_page, "Expired") + self.assertContains(home_page, "DNS needed") + + def test_unknown_domain_does_not_show_as_expired_on_detail_page(self): + """An UNKNOWN domain does not show as expired on the detail page. + It shows as 'DNS needed'""" + # At the time of this test's writing, there are 6 UNKNOWN domains inherited + # from constructors. Let's reset. + with less_console_noise(): + Domain.objects.all().delete() + UserDomainRole.objects.all().delete() + + self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + self.role, _ = UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER + ) + + home_page = self.app.get("/") + self.assertContains(home_page, "igorville.gov") + igorville = Domain.objects.get(name="igorville.gov") + self.assertEquals(igorville.state, Domain.State.UNKNOWN) + detail_page = home_page.click("Manage", index=0) + self.assertNotContains(detail_page, "Expired") + + self.assertContains(detail_page, "DNS needed") + + def test_domain_detail_blocked_for_ineligible_user(self): + """We could easily duplicate this test for all domain management + views, but a single url test should be solid enough since all domain + management pages share the same permissions class""" + with less_console_noise(): + self.user.status = User.RESTRICTED + self.user.save() + home_page = self.app.get("/") + self.assertContains(home_page, "igorville.gov") + response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) + self.assertEqual(response.status_code, 403) + + def test_domain_detail_allowed_for_on_hold(self): + """Test that the domain overview page displays for on hold domain""" + with less_console_noise(): + home_page = self.app.get("/") + self.assertContains(home_page, "on-hold.gov") + + # View domain overview page + detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_on_hold.id})) + self.assertNotContains(detail_page, "Edit") + + def test_domain_detail_see_just_nameserver(self): + with less_console_noise(): + home_page = self.app.get("/") + self.assertContains(home_page, "justnameserver.com") + + # View nameserver on Domain Overview page + detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_just_nameserver.id})) + + self.assertContains(detail_page, "justnameserver.com") + self.assertContains(detail_page, "ns1.justnameserver.com") + self.assertContains(detail_page, "ns2.justnameserver.com") + + def test_domain_detail_see_nameserver_and_ip(self): + with less_console_noise(): + home_page = self.app.get("/") + self.assertContains(home_page, "nameserverwithip.gov") + + # View nameserver on Domain Overview page + detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_with_ip.id})) + + self.assertContains(detail_page, "nameserverwithip.gov") + + self.assertContains(detail_page, "ns1.nameserverwithip.gov") + self.assertContains(detail_page, "ns2.nameserverwithip.gov") + self.assertContains(detail_page, "ns3.nameserverwithip.gov") + # Splitting IP addresses bc there is odd whitespace and can't strip text + self.assertContains(detail_page, "(1.2.3.4,") + self.assertContains(detail_page, "2.3.4.5)") + + def test_domain_detail_with_no_information_or_application(self): + """Test that domain management page returns 200 and displays error + when no domain information or domain application exist""" + with less_console_noise(): + # have to use staff user for this test + staff_user = create_user() + # staff_user.save() + self.client.force_login(staff_user) + + # need to set the analyst_action and analyst_action_location + # in the session to emulate user clicking Manage Domain + # in the admin interface + session = self.client.session + session["analyst_action"] = "foo" + session["analyst_action_location"] = self.domain_no_information.id + session.save() + + detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_no_information.id})) + + self.assertContains(detail_page, "noinformation.gov") + self.assertContains(detail_page, "Domain missing domain information") + + +class TestDomainManagers(TestDomainOverview): + def tearDown(self): + """Ensure that the user has its original permissions""" + super().tearDown() + self.user.is_staff = False + self.user.save() + + def test_domain_managers(self): + response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) + self.assertContains(response, "Domain managers") + + def test_domain_managers_add_link(self): + """Button to get to user add page works.""" + management_page = self.app.get(reverse("domain-users", kwargs={"pk": self.domain.id})) + add_page = management_page.click("Add a domain manager") + self.assertContains(add_page, "Add a domain manager") + + def test_domain_user_add(self): + response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + self.assertContains(response, "Add a domain manager") + + @boto3_mocking.patching + def test_domain_user_add_form(self): + """Adding an existing user works.""" + other_user, _ = get_user_model().objects.get_or_create(email="mayor@igorville.gov") + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + add_page.form["email"] = "mayor@igorville.gov" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + success_result = add_page.form.submit() + + self.assertEqual(success_result.status_code, 302) + self.assertEqual( + success_result["Location"], + reverse("domain-users", kwargs={"pk": self.domain.id}), + ) + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_page = success_result.follow() + self.assertContains(success_page, "mayor@igorville.gov") + + @boto3_mocking.patching + def test_domain_invitation_created(self): + """Add user on a nonexistent email creates an invitation. + + Adding a non-existent user sends an email as a side-effect, so mock + out the boto3 SES email sending here. + """ + # make sure there is no user with this email + email_address = "mayor@igorville.gov" + User.objects.filter(email=email_address).delete() + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + success_result = add_page.form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_page = success_result.follow() + + self.assertContains(success_page, email_address) + self.assertContains(success_page, "Cancel") # link to cancel invitation + self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists()) + + @boto3_mocking.patching + def test_domain_invitation_created_for_caps_email(self): + """Add user on a nonexistent email with CAPS creates an invitation to lowercase email. + + Adding a non-existent user sends an email as a side-effect, so mock + out the boto3 SES email sending here. + """ + # make sure there is no user with this email + email_address = "mayor@igorville.gov" + caps_email_address = "MAYOR@igorville.gov" + User.objects.filter(email=email_address).delete() + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = caps_email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + success_result = add_page.form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_page = success_result.follow() + + self.assertContains(success_page, email_address) + self.assertContains(success_page, "Cancel") # link to cancel invitation + self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists()) + + @boto3_mocking.patching + def test_domain_invitation_email_sent(self): + """Inviting a non-existent user sends them an email.""" + # make sure there is no user with this email + email_address = "mayor@igorville.gov" + User.objects.filter(email=email_address).delete() + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + mock_client = MagicMock() + mock_client_instance = mock_client.return_value + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit() + + # check the mock instance to see if `send_email` was called right + mock_client_instance.send_email.assert_called_once_with( + FromEmailAddress=settings.DEFAULT_FROM_EMAIL, + Destination={"ToAddresses": [email_address]}, + Content=ANY, + ) + + @boto3_mocking.patching + def test_domain_invitation_email_has_email_as_requestor_non_existent(self): + """Inviting a non existent user sends them an email, with email as the name.""" + # make sure there is no user with this email + email_address = "mayor@igorville.gov" + User.objects.filter(email=email_address).delete() + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + mock_client = MagicMock() + mock_client_instance = mock_client.return_value + + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit() + + # check the mock instance to see if `send_email` was called right + mock_client_instance.send_email.assert_called_once_with( + FromEmailAddress=settings.DEFAULT_FROM_EMAIL, + Destination={"ToAddresses": [email_address]}, + Content=ANY, + ) + + # Check the arguments passed to send_email method + _, kwargs = mock_client_instance.send_email.call_args + + # Extract the email content, and check that the message is as we expect + email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + self.assertIn("info@example.com", email_content) + + # Check that the requestors first/last name do not exist + self.assertNotIn("First", email_content) + self.assertNotIn("Last", email_content) + self.assertNotIn("First Last", email_content) + + @boto3_mocking.patching + def test_domain_invitation_email_has_email_as_requestor(self): + """Inviting a user sends them an email, with email as the name.""" + # Create a fake user object + email_address = "mayor@igorville.gov" + User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com") + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + mock_client = MagicMock() + mock_client_instance = mock_client.return_value + + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit() + + # check the mock instance to see if `send_email` was called right + mock_client_instance.send_email.assert_called_once_with( + FromEmailAddress=settings.DEFAULT_FROM_EMAIL, + Destination={"ToAddresses": [email_address]}, + Content=ANY, + ) + + # Check the arguments passed to send_email method + _, kwargs = mock_client_instance.send_email.call_args + + # Extract the email content, and check that the message is as we expect + email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + self.assertIn("info@example.com", email_content) + + # Check that the requestors first/last name do not exist + self.assertNotIn("First", email_content) + self.assertNotIn("Last", email_content) + self.assertNotIn("First Last", email_content) + + @boto3_mocking.patching + def test_domain_invitation_email_has_email_as_requestor_staff(self): + """Inviting a user sends them an email, with email as the name.""" + # Create a fake user object + email_address = "mayor@igorville.gov" + User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com") + + # Make sure the user is staff + self.user.is_staff = True + self.user.save() + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + mock_client = MagicMock() + mock_client_instance = mock_client.return_value + + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit() + + # check the mock instance to see if `send_email` was called right + mock_client_instance.send_email.assert_called_once_with( + FromEmailAddress=settings.DEFAULT_FROM_EMAIL, + Destination={"ToAddresses": [email_address]}, + Content=ANY, + ) + + # Check the arguments passed to send_email method + _, kwargs = mock_client_instance.send_email.call_args + + # Extract the email content, and check that the message is as we expect + email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + self.assertIn("help@get.gov", email_content) + + # Check that the requestors first/last name do not exist + self.assertNotIn("First", email_content) + self.assertNotIn("Last", email_content) + self.assertNotIn("First Last", email_content) + + @boto3_mocking.patching + def test_domain_invitation_email_displays_error_non_existent(self): + """Inviting a non existent user sends them an email, with email as the name.""" + # make sure there is no user with this email + email_address = "mayor@igorville.gov" + User.objects.filter(email=email_address).delete() + + # Give the user who is sending the email an invalid email address + self.user.email = "" + self.user.save() + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + mock_client = MagicMock() + mock_error_message = MagicMock() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with patch("django.contrib.messages.error") as mock_error_message: + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit().follow() + + expected_message_content = "Can't send invitation email. No email is associated with your account." + + # Grab the message content + returned_error_message = mock_error_message.call_args[0][1] + + # Check that the message content is what we expect + self.assertEqual(expected_message_content, returned_error_message) + + @boto3_mocking.patching + def test_domain_invitation_email_displays_error(self): + """When the requesting user has no email, an error is displayed""" + # make sure there is no user with this email + # Create a fake user object + email_address = "mayor@igorville.gov" + User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com") + + # Give the user who is sending the email an invalid email address + self.user.email = "" + self.user.save() + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + mock_client = MagicMock() + + mock_error_message = MagicMock() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with patch("django.contrib.messages.error") as mock_error_message: + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit().follow() + + expected_message_content = "Can't send invitation email. No email is associated with your account." + + # Grab the message content + returned_error_message = mock_error_message.call_args[0][1] + + # Check that the message content is what we expect + self.assertEqual(expected_message_content, returned_error_message) + + def test_domain_invitation_cancel(self): + """Posting to the delete view deletes an invitation.""" + email_address = "mayor@igorville.gov" + invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address) + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) + mock_client.EMAILS_SENT.clear() + with self.assertRaises(DomainInvitation.DoesNotExist): + DomainInvitation.objects.get(id=invitation.id) + + def test_domain_invitation_cancel_no_permissions(self): + """Posting to the delete view as a different user should fail.""" + email_address = "mayor@igorville.gov" + invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address) + + other_user = User() + other_user.save() + self.client.force_login(other_user) + mock_client = MagicMock() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): # permission denied makes console errors + result = self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) + + self.assertEqual(result.status_code, 403) + + @boto3_mocking.patching + def test_domain_invitation_flow(self): + """Send an invitation to a new user, log in and load the dashboard.""" + email_address = "mayor@igorville.gov" + User.objects.filter(email=email_address).delete() + + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + mock_client = MagicMock() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + add_page.form.submit() + + # user was invited, create them + new_user = User.objects.create(username=email_address, email=email_address) + # log them in to `self.app` + self.app.set_user(new_user.username) + # and manually call the on each login callback + new_user.on_each_login() + + # Now load the home page and make sure our domain appears there + home_page = self.app.get(reverse("home")) + self.assertContains(home_page, self.domain.name) + + +class TestDomainNameservers(TestDomainOverview): + def test_domain_nameservers(self): + """Can load domain's nameservers page.""" + page = self.client.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + self.assertContains(page, "DNS name servers") + + def test_domain_nameservers_form_submit_one_nameserver(self): + """Nameserver form submitted with one nameserver throws error. + + Uses self.app WebTest because we need to interact with forms. + """ + # initial nameservers page has one server with two ips + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form with only one nameserver, should error + # regarding required fields + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the required field. form requires a minimum of 2 name servers + self.assertContains( + result, + "At least two name servers are required.", + count=2, + status_code=200, + ) + + def test_domain_nameservers_form_submit_subdomain_missing_ip(self): + """Nameserver form catches missing ip error on subdomain. + + Uses self.app WebTest because we need to interact with forms. + """ + # initial nameservers page has one server with two ips + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without two hosts, both subdomains, + # only one has ips + nameservers_page.form["form-1-server"] = "ns2.igorville.gov" + + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the required field. subdomain missing an ip + self.assertContains( + result, + str(NameserverError(code=NameserverErrorCodes.MISSING_IP)), + count=2, + status_code=200, + ) + + def test_domain_nameservers_form_submit_missing_host(self): + """Nameserver form catches error when host is missing. + + Uses self.app WebTest because we need to interact with forms. + """ + # initial nameservers page has one server with two ips + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without two hosts, both subdomains, + # only one has ips + nameservers_page.form["form-1-ip"] = "127.0.0.1" + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the required field. nameserver has ip but missing host + self.assertContains( + result, + str(NameserverError(code=NameserverErrorCodes.MISSING_HOST)), + count=2, + status_code=200, + ) + + def test_domain_nameservers_form_submit_duplicate_host(self): + """Nameserver form catches error when host is duplicated. + + Uses self.app WebTest because we need to interact with forms. + """ + # initial nameservers page has one server with two ips + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form with duplicate host names of fake.host.com + nameservers_page.form["form-0-ip"] = "" + nameservers_page.form["form-1-server"] = "fake.host.com" + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the required field. remove duplicate entry + self.assertContains( + result, + str(NameserverError(code=NameserverErrorCodes.DUPLICATE_HOST)), + count=2, + status_code=200, + ) + + def test_domain_nameservers_form_submit_whitespace(self): + """Nameserver form removes whitespace from ip. + + Uses self.app WebTest because we need to interact with forms. + """ + nameserver1 = "ns1.igorville.gov" + nameserver2 = "ns2.igorville.gov" + valid_ip = "1.1. 1.1" + # initial nameservers page has one server with two ips + # have to throw an error in order to test that the whitespace has been stripped from ip + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without one host and an ip with whitespace + nameservers_page.form["form-0-server"] = nameserver1 + nameservers_page.form["form-1-ip"] = valid_ip + nameservers_page.form["form-1-server"] = nameserver2 + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a post with an ip address which has been stripped of whitespace, + # response should be a 302 to success page + self.assertEqual(result.status_code, 302) + self.assertEqual( + result["Location"], + reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}), + ) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + page = result.follow() + # in the event of a generic nameserver error from registry error, there will be a 302 + # with an error message displayed, so need to follow 302 and test for success message + self.assertContains(page, "The name servers for this domain have been updated") + + def test_domain_nameservers_form_submit_glue_record_not_allowed(self): + """Nameserver form catches error when IP is present + but host not subdomain. + + Uses self.app WebTest because we need to interact with forms. + """ + nameserver1 = "ns1.igorville.gov" + nameserver2 = "ns2.igorville.com" + valid_ip = "127.0.0.1" + # initial nameservers page has one server with two ips + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without two hosts, both subdomains, + # only one has ips + nameservers_page.form["form-0-server"] = nameserver1 + nameservers_page.form["form-1-server"] = nameserver2 + nameservers_page.form["form-1-ip"] = valid_ip + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the required field. nameserver has ip but missing host + self.assertContains( + result, + str(NameserverError(code=NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED)), + count=2, + status_code=200, + ) + + def test_domain_nameservers_form_submit_invalid_ip(self): + """Nameserver form catches invalid IP on submission. + + Uses self.app WebTest because we need to interact with forms. + """ + nameserver = "ns2.igorville.gov" + invalid_ip = "123" + # initial nameservers page has one server with two ips + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without two hosts, both subdomains, + # only one has ips + nameservers_page.form["form-1-server"] = nameserver + nameservers_page.form["form-1-ip"] = invalid_ip + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the required field. nameserver has ip but missing host + self.assertContains( + result, + str(NameserverError(code=NameserverErrorCodes.INVALID_IP, nameserver=nameserver)), + count=2, + status_code=200, + ) + + def test_domain_nameservers_form_submit_invalid_host(self): + """Nameserver form catches invalid host on submission. + + Uses self.app WebTest because we need to interact with forms. + """ + nameserver = "invalid-nameserver.gov" + valid_ip = "123.2.45.111" + # initial nameservers page has one server with two ips + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without two hosts, both subdomains, + # only one has ips + nameservers_page.form["form-1-server"] = nameserver + nameservers_page.form["form-1-ip"] = valid_ip + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the required field. nameserver has invalid host + self.assertContains( + result, + str(NameserverError(code=NameserverErrorCodes.INVALID_HOST, nameserver=nameserver)), + count=2, + status_code=200, + ) + + def test_domain_nameservers_form_submits_successfully(self): + """Nameserver form submits successfully with valid input. + + Uses self.app WebTest because we need to interact with forms. + """ + nameserver1 = "ns1.igorville.gov" + nameserver2 = "ns2.igorville.gov" + valid_ip = "127.0.0.1" + # initial nameservers page has one server with two ips + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without two hosts, both subdomains, + # only one has ips + nameservers_page.form["form-0-server"] = nameserver1 + nameservers_page.form["form-1-server"] = nameserver2 + nameservers_page.form["form-1-ip"] = valid_ip + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a successful post, response should be a 302 + self.assertEqual(result.status_code, 302) + self.assertEqual( + result["Location"], + reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}), + ) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + page = result.follow() + self.assertContains(page, "The name servers for this domain have been updated") + + def test_domain_nameservers_form_invalid(self): + """Nameserver form does not submit with invalid data. + + Uses self.app WebTest because we need to interact with forms. + """ + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # first two nameservers are required, so if we empty one out we should + # get a form error + nameservers_page.form["form-0-server"] = "" + with less_console_noise(): # swallow logged warning message + result = nameservers_page.form.submit() + # form submission was a post with an error, response should be a 200 + # error text appears four times, twice at the top of the page, + # once around each required field. + self.assertContains( + result, + "At least two name servers are required.", + count=4, + status_code=200, + ) + + +class TestDomainAuthorizingOfficial(TestDomainOverview): + def test_domain_authorizing_official(self): + """Can load domain's authorizing official page.""" + page = self.client.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) + # once on the sidebar, once in the title + self.assertContains(page, "Authorizing official", count=2) + + def test_domain_authorizing_official_content(self): + """Authorizing official information appears on the page.""" + self.domain_information.authorizing_official = Contact(first_name="Testy") + self.domain_information.authorizing_official.save() + self.domain_information.save() + page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) + self.assertContains(page, "Testy") + + def test_domain_edit_authorizing_official_in_place(self): + """When editing an authorizing official for domain information and AO is not + joined to any other objects""" + self.domain_information.authorizing_official = Contact( + first_name="Testy", last_name="Tester", title="CIO", email="nobody@igorville.gov" + ) + self.domain_information.authorizing_official.save() + self.domain_information.save() + ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + ao_form = ao_page.forms[0] + self.assertEqual(ao_form["first_name"].value, "Testy") + ao_form["first_name"] = "Testy2" + # ao_pk is the initial pk of the authorizing official. set it before update + # to be able to verify after update that the same contact object is in place + ao_pk = self.domain_information.authorizing_official.id + ao_form.submit() + + # refresh domain information + self.domain_information.refresh_from_db() + self.assertEqual("Testy2", self.domain_information.authorizing_official.first_name) + self.assertEqual(ao_pk, self.domain_information.authorizing_official.id) + + def test_domain_edit_authorizing_official_creates_new(self): + """When editing an authorizing official for domain information and AO IS + joined to another object""" + # set AO and Other Contact to the same Contact object + self.domain_information.authorizing_official = Contact( + first_name="Testy", last_name="Tester", title="CIO", email="nobody@igorville.gov" + ) + self.domain_information.authorizing_official.save() + self.domain_information.save() + self.domain_information.other_contacts.add(self.domain_information.authorizing_official) + self.domain_information.save() + # load the Authorizing Official in the web form + ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + ao_form = ao_page.forms[0] + # verify the first name is "Testy" and then change it to "Testy2" + self.assertEqual(ao_form["first_name"].value, "Testy") + ao_form["first_name"] = "Testy2" + # ao_pk is the initial pk of the authorizing official. set it before update + # to be able to verify after update that the same contact object is in place + ao_pk = self.domain_information.authorizing_official.id + ao_form.submit() + + # refresh domain information + self.domain_information.refresh_from_db() + # assert that AO information is updated, and that the AO is a new Contact + self.assertEqual("Testy2", self.domain_information.authorizing_official.first_name) + self.assertNotEqual(ao_pk, self.domain_information.authorizing_official.id) + # assert that the Other Contact information is not updated and that the Other Contact + # is the original Contact object + other_contact = self.domain_information.other_contacts.all()[0] + self.assertEqual("Testy", other_contact.first_name) + self.assertEqual(ao_pk, other_contact.id) + + +class TestDomainOrganization(TestDomainOverview): + def test_domain_org_name_address(self): + """Can load domain's org name and mailing address page.""" + page = self.client.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) + # once on the sidebar, once in the page title, once as H1 + self.assertContains(page, "Organization name and mailing address", count=3) + + def test_domain_org_name_address_content(self): + """Org name and address information appears on the page.""" + self.domain_information.organization_name = "Town of Igorville" + self.domain_information.save() + page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) + self.assertContains(page, "Town of Igorville") + + def test_domain_org_name_address_form(self): + """Submitting changes works on the org name address page.""" + self.domain_information.organization_name = "Town of Igorville" + self.domain_information.save() + org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + org_name_page.form["organization_name"] = "Not igorville" + org_name_page.form["city"] = "Faketown" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_result_page = org_name_page.form.submit() + self.assertEqual(success_result_page.status_code, 200) + + self.assertContains(success_result_page, "Not igorville") + self.assertContains(success_result_page, "Faketown") + + +class TestDomainContactInformation(TestDomainOverview): + def test_domain_your_contact_information(self): + """Can load domain's your contact information page.""" + page = self.client.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})) + self.assertContains(page, "Your contact information") + + def test_domain_your_contact_information_content(self): + """Logged-in user's contact information appears on the page.""" + self.user.contact.first_name = "Testy" + self.user.contact.save() + page = self.app.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})) + self.assertContains(page, "Testy") + + +class TestDomainSecurityEmail(TestDomainOverview): + def test_domain_security_email_existing_security_contact(self): + """Can load domain's security email page.""" + with less_console_noise(): + self.mockSendPatch = patch("registrar.models.domain.registry.send") + self.mockedSendFunction = self.mockSendPatch.start() + self.mockedSendFunction.side_effect = self.mockSend + + domain_contact, _ = Domain.objects.get_or_create(name="freeman.gov") + # Add current user to this domain + _ = UserDomainRole(user=self.user, domain=domain_contact, role="admin").save() + page = self.client.get(reverse("domain-security-email", kwargs={"pk": domain_contact.id})) + + # Loads correctly + self.assertContains(page, "Security email") + self.assertContains(page, "security@mail.gov") + self.mockSendPatch.stop() + + def test_domain_security_email_no_security_contact(self): + """Loads a domain with no defined security email. + We should not show the default.""" + with less_console_noise(): + self.mockSendPatch = patch("registrar.models.domain.registry.send") + self.mockedSendFunction = self.mockSendPatch.start() + self.mockedSendFunction.side_effect = self.mockSend + + page = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) + + # Loads correctly + self.assertContains(page, "Security email") + self.assertNotContains(page, "dotgov@cisa.dhs.gov") + self.mockSendPatch.stop() + + def test_domain_security_email(self): + """Can load domain's security email page.""" + with less_console_noise(): + page = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) + self.assertContains(page, "Security email") + + def test_domain_security_email_form(self): + """Adding a security email works. + Uses self.app WebTest because we need to interact with forms. + """ + with less_console_noise(): + security_email_page = self.app.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + security_email_page.form["security_email"] = "mayor@igorville.gov" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + mock_client = MagicMock() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): # swallow log warning message + result = security_email_page.form.submit() + self.assertEqual(result.status_code, 302) + self.assertEqual( + result["Location"], + reverse("domain-security-email", kwargs={"pk": self.domain.id}), + ) + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_page = result.follow() + self.assertContains(success_page, "The security email for this domain has been updated") + + def test_domain_security_email_form_messages(self): + """ + Test against the success and error messages that are defined in the view + """ + with less_console_noise(): + p = "adminpass" + self.client.login(username="superuser", password=p) + form_data_registry_error = { + "security_email": "test@failCreate.gov", + } + form_data_contact_error = { + "security_email": "test@contactError.gov", + } + form_data_success = { + "security_email": "test@something.gov", + } + test_cases = [ + ( + "RegistryError", + form_data_registry_error, + str(GenericError(code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY)), + ), + ( + "ContactError", + form_data_contact_error, + str(SecurityEmailError(code=SecurityEmailErrorCodes.BAD_DATA)), + ), + ( + "RegistrySuccess", + form_data_success, + "The security email for this domain has been updated.", + ), + # Add more test cases with different scenarios here + ] + for test_name, data, expected_message in test_cases: + response = self.client.post( + reverse("domain-security-email", kwargs={"pk": self.domain.id}), + data=data, + follow=True, + ) + # Check the response status code, content, or any other relevant assertions + self.assertEqual(response.status_code, 200) + # Check if the expected message tag is set + if test_name == "RegistryError" or test_name == "ContactError": + message_tag = "error" + elif test_name == "RegistrySuccess": + message_tag = "success" + else: + # Handle other cases if needed + message_tag = "info" # Change to the appropriate default + # Check the message tag + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + message = messages[0] + self.assertEqual(message.tags, message_tag) + self.assertEqual(message.message.strip(), expected_message.strip()) + + def test_domain_overview_blocked_for_ineligible_user(self): + """We could easily duplicate this test for all domain management + views, but a single url test should be solid enough since all domain + management pages share the same permissions class""" + self.user.status = User.RESTRICTED + self.user.save() + home_page = self.app.get("/") + self.assertContains(home_page, "igorville.gov") + with less_console_noise(): + response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) + self.assertEqual(response.status_code, 403) + + +class TestDomainDNSSEC(TestDomainOverview): + + """MockEPPLib is already inherited.""" + + def test_dnssec_page_refreshes_enable_button(self): + """DNSSEC overview page loads when domain has no DNSSEC data + and shows a 'Enable DNSSEC' button.""" + + page = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id})) + self.assertContains(page, "Enable DNSSEC") + + def test_dnssec_page_loads_with_data_in_domain(self): + """DNSSEC overview page loads when domain has DNSSEC data + and the template contains a button to disable DNSSEC.""" + + page = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain_multdsdata.id})) + self.assertContains(page, "Disable DNSSEC") + + # Prepare the data for the POST request + post_data = { + "disable_dnssec": "Disable DNSSEC", + } + updated_page = self.client.post( + reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}), + post_data, + follow=True, + ) + + self.assertEqual(updated_page.status_code, 200) + + self.assertContains(updated_page, "Enable DNSSEC") + + def test_ds_form_loads_with_no_domain_data(self): + """DNSSEC Add DS data page loads when there is no + domain DNSSEC data and shows a button to Add new record""" + + page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dnssec_none.id})) + self.assertContains(page, "You have no DS data added") + self.assertContains(page, "Add new record") + + def test_ds_form_loads_with_ds_data(self): + """DNSSEC Add DS data page loads when there is + domain DNSSEC DS data and shows the data""" + + page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) + self.assertContains(page, "DS data record 1") + + def test_ds_data_form_modal(self): + """When user clicks on save, a modal pops up.""" + add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) + # Assert that a hidden trigger for the modal does not exist. + # This hidden trigger will pop on the page when certain condition are met: + # 1) Initial form contained DS data, 2) All data is deleted and form is + # submitted. + self.assertNotContains(add_data_page, "Trigger Disable DNSSEC Modal") + # Simulate a delete all data + form_data = {} + response = self.client.post( + reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}), + data=form_data, + ) + self.assertEqual(response.status_code, 200) # Adjust status code as needed + # Now check to see whether the JS trigger for the modal is present on the page + self.assertContains(response, "Trigger Disable DNSSEC Modal") + + def test_ds_data_form_submits(self): + """DS data form submits successfully + + Uses self.app WebTest because we need to interact with forms. + """ + add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + with less_console_noise(): # swallow log warning message + result = add_data_page.forms[0].submit() + # form submission was a post, response should be a redirect + self.assertEqual(result.status_code, 302) + self.assertEqual( + result["Location"], + reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}), + ) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + page = result.follow() + self.assertContains(page, "The DS data records for this domain have been updated.") + + def test_ds_data_form_invalid(self): + """DS data form errors with invalid data (missing required fields) + + Uses self.app WebTest because we need to interact with forms. + """ + add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # all four form fields are required, so will test with each blank + add_data_page.forms[0]["form-0-key_tag"] = "" + add_data_page.forms[0]["form-0-algorithm"] = "" + add_data_page.forms[0]["form-0-digest_type"] = "" + add_data_page.forms[0]["form-0-digest"] = "" + with less_console_noise(): # swallow logged warning message + result = add_data_page.forms[0].submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the field. + self.assertContains(result, "Key tag is required", count=2, status_code=200) + self.assertContains(result, "Algorithm is required", count=2, status_code=200) + self.assertContains(result, "Digest type is required", count=2, status_code=200) + self.assertContains(result, "Digest is required", count=2, status_code=200) + + def test_ds_data_form_invalid_keytag(self): + """DS data form errors with invalid data (key tag too large) + + Uses self.app WebTest because we need to interact with forms. + """ + add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # first two nameservers are required, so if we empty one out we should + # get a form error + add_data_page.forms[0]["form-0-key_tag"] = "65536" # > 65535 + add_data_page.forms[0]["form-0-algorithm"] = "" + add_data_page.forms[0]["form-0-digest_type"] = "" + add_data_page.forms[0]["form-0-digest"] = "" + with less_console_noise(): # swallow logged warning message + result = add_data_page.forms[0].submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the field. + self.assertContains( + result, str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE)), count=2, status_code=200 + ) + + def test_ds_data_form_invalid_digest_chars(self): + """DS data form errors with invalid data (digest contains non hexadecimal chars) + + Uses self.app WebTest because we need to interact with forms. + """ + add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # first two nameservers are required, so if we empty one out we should + # get a form error + add_data_page.forms[0]["form-0-key_tag"] = "1234" + add_data_page.forms[0]["form-0-algorithm"] = "3" + add_data_page.forms[0]["form-0-digest_type"] = "1" + add_data_page.forms[0]["form-0-digest"] = "GG1234" + with less_console_noise(): # swallow logged warning message + result = add_data_page.forms[0].submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the field. + self.assertContains( + result, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_CHARS)), count=2, status_code=200 + ) + + def test_ds_data_form_invalid_digest_sha1(self): + """DS data form errors with invalid data (digest is invalid sha-1) + + Uses self.app WebTest because we need to interact with forms. + """ + add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # first two nameservers are required, so if we empty one out we should + # get a form error + add_data_page.forms[0]["form-0-key_tag"] = "1234" + add_data_page.forms[0]["form-0-algorithm"] = "3" + add_data_page.forms[0]["form-0-digest_type"] = "1" # SHA-1 + add_data_page.forms[0]["form-0-digest"] = "A123" + with less_console_noise(): # swallow logged warning message + result = add_data_page.forms[0].submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the field. + self.assertContains( + result, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA1)), count=2, status_code=200 + ) + + def test_ds_data_form_invalid_digest_sha256(self): + """DS data form errors with invalid data (digest is invalid sha-256) + + Uses self.app WebTest because we need to interact with forms. + """ + add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # first two nameservers are required, so if we empty one out we should + # get a form error + add_data_page.forms[0]["form-0-key_tag"] = "1234" + add_data_page.forms[0]["form-0-algorithm"] = "3" + add_data_page.forms[0]["form-0-digest_type"] = "2" # SHA-256 + add_data_page.forms[0]["form-0-digest"] = "GG1234" + with less_console_noise(): # swallow logged warning message + result = add_data_page.forms[0].submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the field. + self.assertContains( + result, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA256)), count=2, status_code=200 + ) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 3924c03c4..a188fb91c 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -3,10 +3,13 @@ import logging from datetime import datetime from registrar.models.domain import Domain from registrar.models.domain_information import DomainInformation -from registrar.models.public_contact import PublicContact -from django.db.models import Value -from django.db.models.functions import Coalesce from django.utils import timezone +from django.core.paginator import Paginator +from django.db.models import F, Value, CharField +from django.db.models.functions import Concat, Coalesce + +from registrar.models.public_contact import PublicContact +from registrar.utility.enums import DefaultEmail logger = logging.getLogger(__name__) @@ -20,50 +23,83 @@ def write_header(writer, columns): def get_domain_infos(filter_condition, sort_fields): - domain_infos = DomainInformation.objects.filter(**filter_condition).order_by(*sort_fields) - return domain_infos + domain_infos = ( + DomainInformation.objects.select_related("domain", "authorizing_official") + .filter(**filter_condition) + .order_by(*sort_fields) + ) + + # Do a mass concat of the first and last name fields for authorizing_official. + # The old operation was computationally heavy for some reason, so if we precompute + # this here, it is vastly more efficient. + domain_infos_cleaned = domain_infos.annotate( + ao=Concat( + Coalesce(F("authorizing_official__first_name"), Value("")), + Value(" "), + Coalesce(F("authorizing_official__last_name"), Value("")), + output_field=CharField(), + ) + ) + return domain_infos_cleaned -def write_row(writer, columns, domain_info): - security_contacts = domain_info.domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY) +def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None): + """Given a set of columns, generate a new row from cleaned column data""" - # For linter - ao = " " - if domain_info.authorizing_official: - first_name = domain_info.authorizing_official.first_name or "" - last_name = domain_info.authorizing_official.last_name or "" - ao = first_name + " " + last_name + # Domain should never be none when parsing this information + if domain_info.domain is None: + raise ValueError("Domain is none") - security_email = " " - if security_contacts: - security_email = security_contacts[0].email + domain = domain_info.domain # type: ignore + + # Grab the security email from a preset dictionary. + # If nothing exists in the dictionary, grab from .contacts. + if security_emails_dict is not None and domain.name in security_emails_dict: + _email = security_emails_dict.get(domain.name) + security_email = _email if _email is not None else " " + else: + # If the dictionary doesn't contain that data, lets filter for it manually. + # This is a last resort as this is a more expensive operation. + security_contacts = domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY) + _email = security_contacts[0].email if security_contacts else None + security_email = _email if _email is not None else " " - invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"} # These are default emails that should not be displayed in the csv report - if security_email is not None and security_email.lower() in invalid_emails: + invalid_emails = {DefaultEmail.LEGACY_DEFAULT.value, DefaultEmail.PUBLIC_CONTACT_DEFAULT.value} + if security_email.lower() in invalid_emails: security_email = "(blank)" + if domain_info.federal_type: + domain_type = f"{domain_info.get_organization_type_display()} - {domain_info.get_federal_type_display()}" + else: + domain_type = domain_info.get_organization_type_display() + # create a dictionary of fields which can be included in output FIELDS = { - "Domain name": domain_info.domain.name, - "Domain type": domain_info.get_organization_type_display() + " - " + domain_info.get_federal_type_display() - if domain_info.federal_type - else domain_info.get_organization_type_display(), + "Domain name": domain.name, + "Domain type": domain_type, "Agency": domain_info.federal_agency, "Organization name": domain_info.organization_name, "City": domain_info.city, "State": domain_info.state_territory, - "AO": ao, + "AO": domain_info.ao, # type: ignore "AO email": domain_info.authorizing_official.email if domain_info.authorizing_official else " ", "Security contact email": security_email, - "Status": domain_info.domain.get_state_display(), - "Expiration date": domain_info.domain.expiration_date, - "Created at": domain_info.domain.created_at, - "First ready": domain_info.domain.first_ready, - "Deleted": domain_info.domain.deleted, + "Status": domain.get_state_display(), + "Expiration date": domain.expiration_date, + "Created at": domain.created_at, + "First ready": domain.first_ready, + "Deleted": domain.deleted, } - writer.writerow([FIELDS.get(column, "") for column in columns]) + # user_emails = [user.email for user in domain.permissions] + + # Dynamically add user emails to the FIELDS dictionary + # for i, user_email in enumerate(user_emails, start=1): + # FIELDS[f"User{i} email"] = user_email + + row = [FIELDS.get(column, "") for column in columns] + return row def write_body( @@ -78,13 +114,51 @@ def write_body( """ # Get the domainInfos - domain_infos = get_domain_infos(filter_condition, sort_fields) + all_domain_infos = get_domain_infos(filter_condition, sort_fields) - all_domain_infos = list(domain_infos) + # Store all security emails to avoid epp calls or excessive filters + sec_contact_ids = all_domain_infos.values_list("domain__security_contact_registry_id", flat=True) + security_emails_dict = {} + public_contacts = ( + PublicContact.objects.only("email", "domain__name") + .select_related("domain") + .filter(registry_id__in=sec_contact_ids) + ) - # Write rows to CSV - for domain_info in all_domain_infos: - write_row(writer, columns, domain_info) + # Populate a dictionary of domain names and their security contacts + for contact in public_contacts: + domain: Domain = contact.domain + if domain is not None and domain.name not in security_emails_dict: + security_emails_dict[domain.name] = contact.email + else: + logger.warning("csv_export -> Domain was none for PublicContact") + + # all_user_nums = 0 + # for domain_info in all_domain_infos: + # user_num = len(domain_info.domain.permissions) + # all_user_nums.append(user_num) + + # if user_num > highest_user_nums: + # highest_user_nums = user_num + + # Build the header here passing to it highest_user_nums + + # Reduce the memory overhead when performing the write operation + paginator = Paginator(all_domain_infos, 1000) + for page_num in paginator.page_range: + page = paginator.page(page_num) + rows = [] + for domain_info in page.object_list: + try: + row = parse_row(columns, domain_info, security_emails_dict) + rows.append(row) + except ValueError: + # This should not happen. If it does, just skip this row. + # It indicates that DomainInformation.domain is None. + logger.error("csv_export -> Error when parsing row, domain was None") + continue + + writer.writerows(rows) def export_data_type_to_csv(csv_file): diff --git a/src/registrar/utility/enums.py b/src/registrar/utility/enums.py index 51f6523c5..706eee1fc 100644 --- a/src/registrar/utility/enums.py +++ b/src/registrar/utility/enums.py @@ -26,3 +26,15 @@ class LogCode(Enum): INFO = 3 DEBUG = 4 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" diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py index c1400d7c0..8785c9076 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -12,6 +12,7 @@ from .domain import ( DomainUsersView, DomainAddUserView, DomainInvitationDeleteView, + DomainDeleteUserView, ) from .health import * from .index import * diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index a15f36ccc..b71018d81 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -159,7 +159,11 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): def storage(self): # marking session as modified on every access # so that updates to nested keys are always saved - self.request.session.modified = True + # Also - check that self.request.session has the attr + # modified to account for test environments calling + # view methods + if hasattr(self.request.session, "modified"): + self.request.session.modified = True return self.request.session.setdefault(self.prefix, {}) @storage.setter @@ -211,6 +215,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): if current_url == self.EDIT_URL_NAME and "id" in kwargs: del self.storage self.storage["application_id"] = kwargs["id"] + self.storage["step_history"] = self.db_check_for_unlocking_steps() # if accessing this class directly, redirect to the first step # in other words, if `ApplicationWizard` is called as view @@ -269,6 +274,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): and from the database if `use_db` is True (provided that record exists). An empty form will be provided if neither of those are true. """ + kwargs = { "files": files, "prefix": self.steps.current, @@ -329,6 +335,43 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): ] return DomainApplication.objects.filter(creator=self.request.user, status__in=check_statuses) + def db_check_for_unlocking_steps(self): + """Helper for get_context_data + + Queries the DB for an application and returns a list of unlocked steps.""" + history_dict = { + "organization_type": self.application.organization_type is not None, + "tribal_government": self.application.tribe_name is not None, + "organization_federal": self.application.federal_type is not None, + "organization_election": self.application.is_election_board is not None, + "organization_contact": ( + self.application.federal_agency is not None + or self.application.organization_name is not None + or self.application.address_line1 is not None + or self.application.city is not None + or self.application.state_territory is not None + or self.application.zipcode is not None + or self.application.urbanization is not None + ), + "about_your_organization": self.application.about_your_organization is not None, + "authorizing_official": self.application.authorizing_official is not None, + "current_sites": ( + self.application.current_websites.exists() or self.application.requested_domain is not None + ), + "dotgov_domain": self.application.requested_domain is not None, + "purpose": self.application.purpose is not None, + "your_contact": self.application.submitter is not None, + "other_contacts": ( + self.application.other_contacts.exists() or self.application.no_other_contacts_rationale is not None + ), + "anything_else": ( + self.application.anything_else is not None or self.application.is_policy_acknowledged is not None + ), + "requirements": self.application.is_policy_acknowledged is not None, + "review": self.application.is_policy_acknowledged is not None, + } + return [key for key, value in history_dict.items() if value] + def get_context_data(self): """Define context for access on all wizard pages.""" # Build the submit button that we'll pass to the modal. @@ -338,6 +381,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): modal_heading = "You are about to submit a domain request for " + str(self.application.requested_domain) else: modal_heading = "You are about to submit an incomplete request" + return { "form_titles": self.TITLES, "steps": self.steps, diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index f3e0a3b9d..04fe1ce3a 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -22,6 +22,7 @@ from registrar.models import ( UserDomainRole, ) from registrar.models.public_contact import PublicContact +from registrar.utility.enums import DefaultEmail from registrar.utility.errors import ( GenericError, GenericErrorCodes, @@ -33,6 +34,7 @@ from registrar.utility.errors import ( SecurityEmailErrorCodes, ) from registrar.models.utility.contact_error import ContactError +from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView from ..forms import ( ContactForm, @@ -141,11 +143,12 @@ class DomainView(DomainBaseView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - default_email = self.object.get_default_security_contact().email - context["default_security_email"] = default_email + default_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value] + + context["hidden_security_emails"] = default_emails 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 return context context["security_email"] = security_email @@ -569,7 +572,7 @@ class DomainSecurityEmailView(DomainFormBaseView): initial = super().get_initial() 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: initial["security_email"] = None return initial @@ -630,6 +633,55 @@ class DomainUsersView(DomainBaseView): 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 = ( + '' + ) + context["modal_button"] = modal_button + + # Create HTML for the modal button when deleting yourself + modal_button_self = ( + '' + ) + context["modal_button_self"] = modal_button_self + + return context + class DomainAddUserView(DomainFormBaseView): """Inside of a domain's user management, a form for adding users. @@ -648,7 +700,7 @@ class DomainAddUserView(DomainFormBaseView): """Get an absolute URL for this domain.""" return self.request.build_absolute_uri(reverse("domain", kwargs={"pk": self.object.id})) - def _send_domain_invitation_email(self, email: str, requester: User, add_success=True): + def _send_domain_invitation_email(self, email: str, requestor: User, add_success=True): """Performs the sending of the domain invitation email, does not make a domain information object email: string- email to send to @@ -656,16 +708,16 @@ class DomainAddUserView(DomainFormBaseView): adding a success message to the view if the email sending succeeds""" # Set a default email address to send to for staff - requester_email = "help@get.gov" + requestor_email = "help@get.gov" - # Check if the email requester has a valid email address - if not requester.is_staff and requester.email is not None and requester.email.strip() != "": - requester_email = requester.email - elif not requester.is_staff: + # Check if the email requestor has a valid email address + if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "": + requestor_email = requestor.email + elif not requestor.is_staff: messages.error(self.request, "Can't send invitation email. No email is associated with your account.") logger.error( f"Can't send email to '{email}' on domain '{self.object}'." - f"No email exists for the requester '{requester.username}'.", + f"No email exists for the requestor '{requestor.username}'.", exc_info=True, ) return None @@ -678,7 +730,7 @@ class DomainAddUserView(DomainFormBaseView): context={ "domain_url": self._domain_abs_url(), "domain": self.object, - "requester_email": requester_email, + "requestor_email": requestor_email, }, ) except EmailSendingError: @@ -693,7 +745,7 @@ class DomainAddUserView(DomainFormBaseView): if add_success: messages.success(self.request, f"{email} has been invited to this domain.") - def _make_invitation(self, email_address: str, requester: User): + def _make_invitation(self, email_address: str, requestor: User): """Make a Domain invitation for this email and redirect with a message.""" invitation, created = DomainInvitation.objects.get_or_create(email=email_address, domain=self.object) if not created: @@ -703,22 +755,22 @@ class DomainAddUserView(DomainFormBaseView): f"{email_address} has already been invited to this domain.", ) else: - self._send_domain_invitation_email(email=email_address, requester=requester) + self._send_domain_invitation_email(email=email_address, requestor=requestor) return redirect(self.get_success_url()) def form_valid(self, form): """Add the specified user on this domain.""" requested_email = form.cleaned_data["email"] - requester = self.request.user + requestor = self.request.user # look up a user with that email try: requested_user = User.objects.get(email=requested_email) except User.DoesNotExist: # no matching user, go make an invitation - return self._make_invitation(requested_email, requester) + return self._make_invitation(requested_email, requestor) else: # if user already exists then just send an email - self._send_domain_invitation_email(requested_email, requester, add_success=False) + self._send_domain_invitation_email(requested_email, requestor, add_success=False) try: UserDomainRole.objects.create( @@ -743,3 +795,60 @@ class DomainInvitationDeleteView(DomainInvitationPermissionDeleteView, SuccessMe def get_success_message(self, cleaned_data): 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 diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 0cf5970df..b2c4cb364 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -286,6 +286,43 @@ class DomainApplicationPermission(PermissionsLoginMixin): 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): """Permission mixin that redirects to withdraw action on domain application diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index 587eb0b5c..54c96d602 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -4,6 +4,7 @@ import abc # abstract base class from django.views.generic import DetailView, DeleteView, TemplateView from registrar.models import Domain, DomainApplication, DomainInvitation +from registrar.models.user_domain_role import UserDomainRole from .mixins import ( DomainPermission, @@ -11,6 +12,7 @@ from .mixins import ( DomainApplicationPermissionWithdraw, DomainInvitationPermission, ApplicationWizardPermission, + UserDeleteDomainRolePermission, ) import logging @@ -130,3 +132,20 @@ class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteV model = 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" diff --git a/src/run.sh b/src/run.sh index 487c54591..1d35cd617 100755 --- a/src/run.sh +++ b/src/run.sh @@ -6,4 +6,4 @@ set -o pipefail # Make sure that django's `collectstatic` has been run locally before pushing up to any environment, # so that the styles and static assets to show up correctly on any environment. -gunicorn registrar.config.wsgi -t 60 +gunicorn --worker-class=gevent registrar.config.wsgi -t 60