From 98b0338a6670797766c72f74e9f5eff6c0e690f2 Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Mon, 7 Oct 2024 16:55:40 -0400 Subject: [PATCH 01/20] Convert tests for Registrar add/edit/delete views. --- perma_web/conftest.py | 8 +- .../perma/tests/test_views_user_management.py | 142 ++++++++++-------- 2 files changed, 89 insertions(+), 61 deletions(-) diff --git a/perma_web/conftest.py b/perma_web/conftest.py index 1a26d4e40..64c046a5b 100644 --- a/perma_web/conftest.py +++ b/perma_web/conftest.py @@ -242,6 +242,12 @@ class Meta: class PendingRegistrarFactory(RegistrarFactory): status = 'pending' + pending_users = factory.RelatedFactoryList( + 'conftest.LinkUserFactory', + size=1, + factory_related_name='pending_registrar' + ) + @register_factory class DeniedRegistrarFactory(RegistrarFactory): @@ -279,7 +285,7 @@ class Meta: first_name = factory.Faker('first_name') last_name = factory.Faker('last_name') - email = factory.Sequence(lambda n: 'user%s@example.com' % n) + email = factory.LazyAttribute(lambda o: f'{o.first_name}_{o.last_name}@example.com') # Default to confirmed and active in the fixtures for convenience is_active = True diff --git a/perma_web/perma/tests/test_views_user_management.py b/perma_web/perma/tests/test_views_user_management.py index ebe96520c..5a0e158a0 100644 --- a/perma_web/perma/tests/test_views_user_management.py +++ b/perma_web/perma/tests/test_views_user_management.py @@ -20,6 +20,88 @@ from perma.tests.utils import PermaTestCase from perma.forms import MultipleUsersFormWithOrganization +from conftest import submit_form + + +### +### REGISTRAR A/E/D VIEWS ### +### + +def test_admin_can_create_registrar(client, admin_user): + client.force_login(admin_user) + submit_form( + client, + 'user_management_manage_registrar', + data={ + 'a-name':'test_views_registrar', + 'a-email':'test@test.com', + 'a-website':'http://test.com' + }, + success_url=reverse('user_management_manage_registrar'), + success_query=Registrar.objects.filter(name='test_views_registrar') + ) + + +def test_admin_can_update_registrar(client, admin_user, registrar): + client.force_login(admin_user) + submit_form( + client, + url=reverse('user_management_manage_single_registrar', args=[registrar.pk]), + data={ + 'a-name': 'new_name', + 'a-email': 'test@test.com2', + 'a-website': 'http://test.com' + }, + success_url=reverse('user_management_manage_registrar'), + success_query=Registrar.objects.filter(name='new_name') + ) + + +def test_registrar_can_update_registrar(client, registrar_user): + client.force_login(registrar_user) + submit_form( + client, + url=reverse('user_management_manage_single_registrar', args=[registrar_user.registrar.pk]), + data={ + 'a-name': 'new_name', + 'a-email': 'test@test.com2', + 'a-website': 'http://test.com' + }, + success_url=reverse('settings_affiliations'), + success_query=Registrar.objects.filter(name='new_name') + ) + + +def test_registrar_cannot_update_unrelated_registrar(client, registrar, registrar_user): + assert registrar_user.registrar_id != registrar.id + client.force_login(registrar_user) + response = client.get( + reverse('user_management_manage_single_registrar', args=[registrar.pk]), + secure=True + ) + assert response.status_code == 403 + + +def test_admin_can_approve_pending_registrar(client, pending_registrar, admin_user): + client.force_login(admin_user) + submit_form( + client, + url=reverse('user_sign_up_approve_pending_registrar', args=[pending_registrar.pk]), + data={'status':'approved', 'base_rate': '100.00'}, + success_query=Registrar.objects.filter(pk=pending_registrar.pk, status="approved").exists() + ) + + +def test_admin_can_deny_pending_registrar(client, pending_registrar, admin_user): + client.force_login(admin_user) + submit_form( + client, + url=reverse('user_sign_up_approve_pending_registrar', args=[pending_registrar.pk]), + data={'status': 'denied', 'base_rate': '100.00'}, + success_query=Registrar.objects.filter(pk=pending_registrar.pk, status="denied").exists() + ) + + class UserManagementViewsTestCase(PermaTestCase): @@ -143,66 +225,6 @@ def test_registrar_user_list_filters(self): self.assertEqual(response.count(b'deactivated account'), 0) self.assertEqual(response.count(b'User must activate account'), 1) - def test_admin_can_create_registrar(self): - self.submit_form( - 'user_management_manage_registrar', { - 'a-name':'test_views_registrar', - 'a-email':'test@test.com', - 'a-website':'http://test.com' - }, - user=self.admin_user, - success_url=reverse('user_management_manage_registrar'), - success_query=Registrar.objects.filter(name='test_views_registrar')) - - def test_admin_can_update_registrar(self): - self.submit_form('user_management_manage_single_registrar', - user=self.admin_user, - reverse_kwargs={'args':[self.unrelated_registrar.pk]}, - data={ - 'a-name': 'new_name', - 'a-email': 'test@test.com2', - 'a-website': 'http://test.com'}, - success_url=reverse('user_management_manage_registrar'), - success_query=Registrar.objects.filter(name='new_name')) - - def test_registrar_can_update_registrar(self): - self.submit_form('user_management_manage_single_registrar', - user=self.registrar_user, - reverse_kwargs={'args': [self.registrar.pk]}, - data={ - 'a-name': 'new_name', - 'a-email': 'test@test.com2', - 'a-website': 'http://test.com'}, - success_url=reverse('settings_affiliations'), - success_query=Registrar.objects.filter(name='new_name')) - - def test_registrar_cannot_update_unrelated_registrar(self): - self.get('user_management_manage_single_registrar', - user=self.registrar_user, - reverse_kwargs={'args': [self.unrelated_registrar.pk]}, - require_status_code=403) - - def test_admin_can_approve_pending_registrar(self): - self.submit_form( - 'user_sign_up_approve_pending_registrar', - user=self.admin_user, - data={'status': 'approved', 'base_rate': '100.00'}, - reverse_kwargs={'args': [self.pending_registrar.pk]}, - success_query=Registrar.objects.filter( - pk=self.pending_registrar.pk, status='approved' - ).exists(), - ) - - def test_admin_can_deny_pending_registrar(self): - self.submit_form( - 'user_sign_up_approve_pending_registrar', - user=self.admin_user, - data={'status': 'denied', 'base_rate': '100.00'}, - reverse_kwargs={'args': [self.pending_registrar.pk]}, - success_query=Registrar.objects.filter( - pk=self.pending_registrar.pk, status='denied' - ).exists(), - ) ### ORGANIZATION A/E/D VIEWS ### From be7090361d45d9b0ac42dc4defba531991861777 Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Tue, 8 Oct 2024 14:49:24 -0400 Subject: [PATCH 02/20] Fix test that was broken, uncovered by changing our fixtures to use capitalized email addresses. --- perma_web/perma/tests/test_views_user_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/perma_web/perma/tests/test_views_user_settings.py b/perma_web/perma/tests/test_views_user_settings.py index 69e8a65a4..e4f52ea2e 100644 --- a/perma_web/perma/tests/test_views_user_settings.py +++ b/perma_web/perma/tests/test_views_user_settings.py @@ -32,7 +32,7 @@ def get_name_and_email(): first_name, last_name, email = get_name_and_email() assert first_name == link_user.first_name assert last_name == link_user.last_name - assert email == link_user.email + assert email == link_user.raw_email # We can submit the change form new_first, new_last, new_email = "Newfirst", "Newlast", "newemail@example.com" From b35ddfcfb575a02f2707a986ebd486d0b5f040b2 Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Tue, 8 Oct 2024 14:52:53 -0400 Subject: [PATCH 03/20] Convert tests for Organization add/edit/delete views. --- perma_web/conftest.py | 9 + .../perma/tests/test_views_user_management.py | 226 +++++++++++------- 2 files changed, 145 insertions(+), 90 deletions(-) diff --git a/perma_web/conftest.py b/perma_web/conftest.py index 64c046a5b..dd7fa3be9 100644 --- a/perma_web/conftest.py +++ b/perma_web/conftest.py @@ -278,6 +278,15 @@ class Meta: registrar = factory.SubFactory(RegistrarFactory) +@register_factory +class OrganizationWithLinksFactory(OrganizationFactory): + links = factory.RelatedFactoryList( + 'conftest.LinkFactory', + size=1, + factory_related_name='organization' + ) + + @register_factory class LinkUserFactory(DjangoModelFactory): class Meta: diff --git a/perma_web/perma/tests/test_views_user_management.py b/perma_web/perma/tests/test_views_user_management.py index 5a0e158a0..a88b9a850 100644 --- a/perma_web/perma/tests/test_views_user_management.py +++ b/perma_web/perma/tests/test_views_user_management.py @@ -102,6 +102,142 @@ def test_admin_can_deny_pending_registrar(client, pending_registrar, admin_user) ) +### +### ORGANIZATION A/E/D VIEWS ### +### + +def test_admin_can_create_organization(client, registrar, admin_user): + client.force_login(admin_user) + submit_form( + client, + 'user_management_manage_organization', + data={ + 'a-name': 'new_name', + 'a-registrar': registrar.pk + }, + success_url=reverse('user_management_manage_organization'), + success_query=Organization.objects.filter(name='new_name') + ) + + +def test_registrar_can_create_organization(client, registrar_user): + client.force_login(registrar_user) + submit_form( + client, + 'user_management_manage_organization', + data={'a-name': 'new_name'}, + success_url=reverse('user_management_manage_organization'), + success_query=Organization.objects.filter(name='new_name') + ) + + +def test_admin_can_update_organization(client, organization, admin_user): + client.force_login(admin_user) + submit_form( + client, + url=reverse('user_management_manage_single_organization', args=[organization.pk]), + data={ + 'a-name': 'new_name', + 'a-registrar': organization.registrar.pk + }, + success_url=reverse('user_management_manage_organization'), + success_query=Organization.objects.filter(name='new_name') + ) + + +def test_registrar_can_update_organization(client, registrar_user): + org = registrar_user.registrar.organizations.first() + client.force_login(registrar_user) + submit_form( + client, + url=reverse('user_management_manage_single_organization', args=[org.pk]), + data={'a-name': 'new_name'}, + success_url=reverse('user_management_manage_organization'), + success_query=Organization.objects.filter(name='new_name') + ) + + +def test_org_user_can_update_organization(client, org_user): + org = org_user.organizations.first() + client.force_login(org_user) + submit_form( + client, + url=reverse('user_management_manage_single_organization', args=[org.pk]), + data={'a-name': 'new_name'}, + success_url=reverse('user_management_manage_organization'), + success_query=Organization.objects.filter(name='new_name') + ) + + +def test_registrar_cannot_update_unrelated_organization(client, registrar_user, organization): + client.force_login(registrar_user) + response = client.get( + reverse('user_management_manage_single_organization', args=[organization.pk]), + secure=True + ) + assert response.status_code == 403 + + +def test_org_user_cannot_update_unrelated_organization(client, org_user, organization_factory): + other_org = organization_factory() + client.force_login(org_user) + response = client.get( + reverse('user_management_manage_single_organization', args=[other_org.pk]), + secure=True + ) + assert response.status_code == 403 + + +def _delete_organization(client, user, org, expect="success"): + url = reverse('user_management_manage_single_organization_delete', args=[org.pk]) + client.force_login(user) + if expect == 'success': + return submit_form( + client, + url=url, + success_url=reverse('user_management_manage_organization'), + success_query=Organization.objects.filter(user_deleted=True, pk=org.pk) + ) + else: + response = submit_form( + client, + url=url + ) + assert response.status_code == expect + return response + + +def test_admin_user_can_delete_organization_without_links(client, admin_user, organization): + _delete_organization(client, admin_user, organization) + _delete_organization(client, admin_user, organization, 404) + + +def test_registrar_user_cannot_delete_unrelated_organization(client, registrar_user, organization): + _delete_organization(client, registrar_user, organization, 403) + + +def test_registrar_user_can_delete_organization_without_links(client, registrar_user): + org = registrar_user.registrar.organizations.first() + _delete_organization(client, registrar_user, org) + _delete_organization(client, registrar_user, org, 404) + + +def test_org_user_cannot_delete_unrelated_organization(client, org_user, organization_factory): + org = organization_factory() + _delete_organization(client, org_user, org, 403) + + +def test_org_user_can_delete_organization_without_links(client, multi_registrar_org_user): + # Use multi_registrar_org_user so that the user is still an org user, even after the org is deleted + user = multi_registrar_org_user + org = user.organizations.first() + _delete_organization(client, user, org) + _delete_organization(client, user, org, 404) + + +def test_even_admin_cannot_delete_organization_with_links(client, admin_user, organization_with_links): + _delete_organization(client, admin_user, organization_with_links, 403) + class UserManagementViewsTestCase(PermaTestCase): @@ -517,96 +653,6 @@ def test_sponsored_user_list_filters(self): # user status filter tested in test_registrar_user_list_filters - - def test_admin_can_create_organization(self): - self.submit_form('user_management_manage_organization', - user=self.admin_user, - data={ - 'a-name': 'new_name', - 'a-registrar': self.registrar.pk}, - success_url=reverse('user_management_manage_organization'), - success_query=Organization.objects.filter(name='new_name')) - - def test_registrar_can_create_organization(self): - self.submit_form('user_management_manage_organization', - user=self.registrar_user, - data={ - 'a-name': 'new_name'}, - success_url=reverse('user_management_manage_organization'), - success_query=Organization.objects.filter(name='new_name')) - - def test_admin_can_update_organization(self): - self.submit_form('user_management_manage_single_organization', - user=self.admin_user, - reverse_kwargs={'args':[self.organization.pk]}, - data={ - 'a-name': 'new_name', - 'a-registrar': self.registrar.pk}, - success_url=reverse('user_management_manage_organization'), - success_query=Organization.objects.filter(name='new_name')) - - def test_registrar_can_update_organization(self): - self.submit_form('user_management_manage_single_organization', - user=self.registrar_user, - reverse_kwargs={'args':[self.organization.pk]}, - data={ - 'a-name': 'new_name'}, - success_url=reverse('user_management_manage_organization'), - success_query=Organization.objects.filter(name='new_name')) - - def test_org_user_can_update_organization(self): - self.submit_form('user_management_manage_single_organization', - user=self.organization_user, - reverse_kwargs={'args': [self.organization.pk]}, - data={ - 'a-name': 'new_name'}, - success_url=reverse('user_management_manage_organization'), - success_query=Organization.objects.filter(name='new_name')) - - def test_registrar_cannot_update_unrelated_organization(self): - self.get('user_management_manage_single_organization', - user=self.registrar_user, - reverse_kwargs={'args': [self.unrelated_organization.pk]}, - require_status_code=403) - - def test_org_user_cannot_update_unrelated_organization(self): - self.get('user_management_manage_single_organization', - user=self.organization_user, - reverse_kwargs={'args': [self.unrelated_organization.pk]}, - require_status_code=403) - - def _delete_organization(self, user, should_succeed=True): - if should_succeed: - self.submit_form('user_management_manage_single_organization_delete', - user=user, - reverse_kwargs={'args': [self.deletable_organization.pk]}, - success_url=reverse('user_management_manage_organization'), - success_query=Organization.objects.filter(user_deleted=True, pk=self.deletable_organization.pk)) - else: - self.submit_form('user_management_manage_single_organization_delete', - user=user, - reverse_kwargs={'args': [self.deletable_organization.pk]}, - require_status_code=404) - - def test_admin_user_can_delete_empty_organization(self): - self._delete_organization(self.admin_user) - self._delete_organization(self.admin_user, False) - - def test_registrar_user_can_delete_empty_organization(self): - self._delete_organization(self.deletable_organization.registrar.users.first()) - self._delete_organization(self.deletable_organization.registrar.users.first(), False) - - def test_org_user_can_delete_empty_organization(self): - self._delete_organization(self.deletable_organization.users.first()) - self._delete_organization(self.deletable_organization.users.first(), False) - - def test_cannot_delete_nonempty_organization(self): - self.submit_form('user_management_manage_single_organization_delete', - user=self.admin_user, - reverse_kwargs={'args': [self.organization.pk]}, - require_status_code=404) - - ### USER A/E/D VIEWS ### def test_user_list_filters(self): From c844df9a39e05f88dafe7c718f844ae3d50984c6 Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Tue, 15 Oct 2024 10:51:31 -0400 Subject: [PATCH 04/20] Add user data fixture. --- perma_web/conftest.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/perma_web/conftest.py b/perma_web/conftest.py index dd7fa3be9..38cb5a25f 100644 --- a/perma_web/conftest.py +++ b/perma_web/conftest.py @@ -172,6 +172,7 @@ def f(page, user): import factory from factory.django import DjangoModelFactory, Password +from faker import Faker import humps from decimal import Decimal @@ -183,6 +184,7 @@ def f(page, user): from perma.utils import pp_date_from_post +FAKE = Faker() GENESIS = datetime.fromtimestamp(0).replace(tzinfo=tz.utc) # this gives us a variable that we can use unhashed in tests TEST_USER_PASSWORD = 'pass' @@ -513,6 +515,19 @@ def generic(self, *args, **kwargs): return UserClient() +@pytest.fixture +def user_data(): + first_name = FAKE.first_name() + last_name = FAKE.last_name() + email = f"{first_name}_{last_name}@example.com" + return { + "first_name": first_name, + "last_name": last_name, + "email": email, + "normalized_email": email.lower() + } + + @pytest.fixture def admin_user(link_user_factory): return link_user_factory(is_staff=True) From 7f827294775b3110e13264d6ba67f8f916133b4d Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Tue, 15 Oct 2024 11:01:05 -0400 Subject: [PATCH 05/20] Lint. --- .../perma/tests/test_views_user_management.py | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/perma_web/perma/tests/test_views_user_management.py b/perma_web/perma/tests/test_views_user_management.py index a88b9a850..2e5ac142e 100644 --- a/perma_web/perma/tests/test_views_user_management.py +++ b/perma_web/perma/tests/test_views_user_management.py @@ -48,9 +48,9 @@ def test_admin_can_update_registrar(client, admin_user, registrar): client, url=reverse('user_management_manage_single_registrar', args=[registrar.pk]), data={ - 'a-name': 'new_name', - 'a-email': 'test@test.com2', - 'a-website': 'http://test.com' + 'a-name': 'new_name', + 'a-email': 'test@test.com2', + 'a-website': 'http://test.com' }, success_url=reverse('user_management_manage_registrar'), success_query=Registrar.objects.filter(name='new_name') @@ -63,9 +63,9 @@ def test_registrar_can_update_registrar(client, registrar_user): client, url=reverse('user_management_manage_single_registrar', args=[registrar_user.registrar.pk]), data={ - 'a-name': 'new_name', - 'a-email': 'test@test.com2', - 'a-website': 'http://test.com' + 'a-name': 'new_name', + 'a-email': 'test@test.com2', + 'a-website': 'http://test.com' }, success_url=reverse('settings_affiliations'), success_query=Registrar.objects.filter(name='new_name') @@ -111,12 +111,12 @@ def test_admin_can_create_organization(client, registrar, admin_user): submit_form( client, 'user_management_manage_organization', - data={ - 'a-name': 'new_name', - 'a-registrar': registrar.pk - }, - success_url=reverse('user_management_manage_organization'), - success_query=Organization.objects.filter(name='new_name') + data={ + 'a-name': 'new_name', + 'a-registrar': registrar.pk + }, + success_url=reverse('user_management_manage_organization'), + success_query=Organization.objects.filter(name='new_name') ) @@ -125,9 +125,9 @@ def test_registrar_can_create_organization(client, registrar_user): submit_form( client, 'user_management_manage_organization', - data={'a-name': 'new_name'}, - success_url=reverse('user_management_manage_organization'), - success_query=Organization.objects.filter(name='new_name') + data={'a-name': 'new_name'}, + success_url=reverse('user_management_manage_organization'), + success_query=Organization.objects.filter(name='new_name') ) @@ -137,8 +137,8 @@ def test_admin_can_update_organization(client, organization, admin_user): client, url=reverse('user_management_manage_single_organization', args=[organization.pk]), data={ - 'a-name': 'new_name', - 'a-registrar': organization.registrar.pk + 'a-name': 'new_name', + 'a-registrar': organization.registrar.pk }, success_url=reverse('user_management_manage_organization'), success_query=Organization.objects.filter(name='new_name') From aa9912b2436271093059f09862786ea285d03f4b Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Tue, 7 Jan 2025 17:33:05 -0500 Subject: [PATCH 06/20] Convert tests for the export views; add a helper for nuking interfering JSON fixtures. --- perma_web/conftest.py | 63 ++++- .../perma/tests/test_views_user_management.py | 257 +++++++++--------- 2 files changed, 184 insertions(+), 136 deletions(-) diff --git a/perma_web/conftest.py b/perma_web/conftest.py index 38cb5a25f..e06cbc78d 100644 --- a/perma_web/conftest.py +++ b/perma_web/conftest.py @@ -99,6 +99,23 @@ def _live_server_db_helper(request): _load_json_fixtures() +@pytest.fixture() +def flush_db(django_db_blocker): + """ + While we are still using django_db_setup with session scope to install the legacy JSON fixtures, + it is occasionally convenient to flush the database before a given test. + + Include this as the FIRST fixture in any test to install any other pytest fixtures into a + blank, clean database. + + The JSON fixtures will be re-installed on teardown. + """ + with django_db_blocker.unblock(): + call_command('flush', verbosity=0, interactive=False) + yield + _load_json_fixtures() + + @pytest.fixture(autouse=True, scope='function') def cleanup_storage(): """ @@ -534,12 +551,13 @@ def admin_user(link_user_factory): @pytest.fixture -def org_user_factory(link_user, organization): +def org_user_factory(link_user_factory, organization_factory): def f(orgs=None): + link_user = link_user_factory() if orgs: link_user.organizations.set(orgs) else: - link_user.organizations.add(organization) + link_user.organizations.add(organization_factory()) return link_user return f @@ -548,6 +566,7 @@ def f(orgs=None): def org_user(org_user_factory): return org_user_factory() + @pytest.fixture def multi_registrar_org_user(org_user_factory, organization_factory): first = organization_factory() @@ -556,6 +575,34 @@ def multi_registrar_org_user(org_user_factory, organization_factory): return org_user_factory(orgs=[first, second]) +@pytest.fixture +def org_user_list(multi_registrar_org_user, org_user_factory): + single_organization_users = [] + for _ in range(5): + single_organization_users.append(org_user_factory()) + + user_data = [] + for user in sorted([multi_registrar_org_user] + single_organization_users, key=lambda u: u.last_name): + for org in user.organizations.all().order_by('name'): + user_data.append((user.email, org.name)) + + return user_data + + +@pytest.fixture +def sponsored_user_list(sponsored_user_factory): + users = [] + for _ in range(5): + users.append(sponsored_user_factory()) + + user_data = [] + for user in sorted(users, key=lambda u: u.last_name): + user_data.append((user.email, user.sponsorships.first().status)) + + return user_data + + + ### For testing customer interactions @pytest.fixture @@ -943,6 +990,18 @@ def f(customer): return f +# For working with registrars + +# For working with organizations + +@pytest.fixture +def org_with_five_users(link_user_factory, organization): + for _ in range(5): + user = link_user_factory() + user.organizations.add(organization) + return organization + + ### For working with links ### @pytest.fixture diff --git a/perma_web/perma/tests/test_views_user_management.py b/perma_web/perma/tests/test_views_user_management.py index 2e5ac142e..f0582eea1 100644 --- a/perma_web/perma/tests/test_views_user_management.py +++ b/perma_web/perma/tests/test_views_user_management.py @@ -3,6 +3,7 @@ import csv from io import StringIO import json +import pytest from random import random, getrandbits import re @@ -239,6 +240,128 @@ def test_even_admin_cannot_delete_organization_with_links(client, admin_user, or _delete_organization(client, admin_user, organization_with_links, 403) +### +### EXPORT LISTS +### + + +@pytest.mark.parametrize( + "export_format,mime_type", + [ + ('csv', 'text/csv'), + ('json', 'application/json') + ] +) +def test_org_export_user_list(export_format, mime_type, client, org_with_five_users): + """Export all users in a single org""" + + # Log in as one of the org's users + user = org_with_five_users.users.order_by('?').first() + client.force_login(user) + + # Get the export output + url = reverse( + 'user_management_manage_single_organization_export_user_list', + args=[org_with_five_users.id] + ) + response = client.get( + url, + data={'format': export_format}, + secure=True + ) + assert response.status_code == 200 + assert response.headers['Content-Type'] == mime_type + + # Parse the output + match export_format: + case 'csv': + csv_file = StringIO(response.content.decode('utf8')) + reader = csv.DictReader(csv_file) + case 'json': + reader = json.loads(response.content) + + # Validate the output against the expected results + reader_record_count = 0 + for record in reader: + assert record['organization_name'] == org_with_five_users.name + reader_record_count += 1 + assert reader_record_count == 5 + + +@pytest.mark.parametrize( + "export_format,mime_type", + [ + ('csv', 'text/csv'), + ('json', 'application/json') + ] +) +def test_organization_user_export_user_list(flush_db, export_format, mime_type, client, admin_user, org_user_list): + """Export all org users accessible to given user""" + # Log in as an admin, to see the full list + client.force_login(admin_user) + + # Get the export output + response = client.get( + reverse('user_management_manage_organization_user_export_user_list'), + data={'format': export_format}, + secure=True + ) + assert response.status_code == 200 + assert response.headers['Content-Type'] == mime_type + + # Parse the output + match export_format: + case 'csv': + csv_file = StringIO(response.content.decode('utf8')) + reader = csv.DictReader(csv_file) + case 'json': + reader = json.loads(response.content) + + # Validate the output against expected results + for index, record in enumerate(reader): + expected_email, expected_organization_name = org_user_list[index] + assert record['email'] == expected_email + assert record['organization_name'] == expected_organization_name + assert index + 1 == len(org_user_list) + + +@pytest.mark.parametrize( + "export_format,mime_type", + [ + ('csv', 'text/csv'), + ('json', 'application/json') + ] +) +def test_sponsored_user_export_user_list(flush_db, export_format, mime_type, client, admin_user, sponsored_user_list): + """Export all sponsored users accessible to given user""" + # Log in as an admin, to see the full list + client.force_login(admin_user) + + # Get the export output + response = client.get( + reverse('user_management_manage_sponsored_user_export_user_list'), + data={'format': export_format}, + secure=True + ) + assert response.status_code == 200 + assert response.headers['Content-Type'] == mime_type + + # Parse the output + match export_format: + case 'csv': + csv_file = StringIO(response.content.decode('utf8')) + reader = csv.DictReader(csv_file) + case 'json': + reader = json.loads(response.content) + + # Validate the output against expected results + for index, record in enumerate(reader): + expected_email, expected_sponsorship_status = sponsored_user_list[index] + assert record['email'] == expected_email + assert record['sponsorship_status'] == expected_sponsorship_status + assert index + 1 == len(sponsored_user_list) + + class UserManagementViewsTestCase(PermaTestCase): @classmethod @@ -466,140 +589,6 @@ def test_org_user_list_filters(self): # status filter tested in test_registrar_user_list_filters - def test_org_export_user_list(self): - expected_results = { - # Org ID: (record count, org name) - 1: (3, 'Test Journal'), - 2: (1, 'Another Journal'), - 3: (3, 'A Third Journal'), - 4: (3, "Another Library's Journal"), - 5: (1, 'Some Case'), - 6: (0, 'Some Other Case'), - } - for org_id, (record_count, org_name) in expected_results.items(): - # Get CSV export output - csv_response: HttpResponse = self.get( - 'user_management_manage_single_organization_export_user_list', - request_kwargs={'data': {'format': 'csv'}}, - reverse_kwargs={'args': [org_id]}, - user=self.admin_user, - ) - self.assertEqual(csv_response.headers['Content-Type'], 'text/csv') - - # Validate CSV output against expected results - csv_file = StringIO(csv_response.content.decode('utf8')) - reader = csv.DictReader(csv_file) - reader_record_count = 0 - for record in reader: - self.assertEqual(record['organization_name'], org_name) - reader_record_count += 1 - self.assertEqual(reader_record_count, record_count) - - # Get JSON export output - json_response: JsonResponse = self.get( - 'user_management_manage_single_organization_export_user_list', - request_kwargs={'data': {'format': 'json'}}, - reverse_kwargs={'args': [org_id]}, - user=self.admin_user, - ) - self.assertEqual(json_response.headers['Content-Type'], 'application/json') - - # Validate JSON output against expected results - reader = json.loads(json_response.content) - reader_record_count = 0 - for record in reader: - self.assertEqual(record['organization_name'], org_name) - reader_record_count += 1 - self.assertEqual(reader_record_count, record_count) - - def test_organization_user_export_user_list(self): - expected_results = [ - ('case_one_lawyer@firm.com', 'Some Case'), - ('multi_registrar_org_user@example.com', 'Another Journal'), - ('multi_registrar_org_user@example.com', "Another Library's Journal"), - ('multi_registrar_org_user@example.com', 'A Third Journal'), - ('multi_registrar_org_user@example.com', 'Test Journal'), - ('test_another_library_org_user@example.com', "Another Library's Journal"), - ('test_another_library_org_user@example.com', 'A Third Journal'), - ('test_yet_another_library_org_user@example.com', "Another Library's Journal"), - ('test_another_org_user@example.com', 'A Third Journal'), - ('test_org_rando_user@example.com', 'Test Journal'), - ('test_org_user@example.com', 'Test Journal'), - ] - - # Get CSV export output - csv_response: HttpResponse = self.get( - 'user_management_manage_organization_user_export_user_list', - request_kwargs={'data': {'format': 'csv'}}, - user=self.admin_user, - ) - self.assertEqual(csv_response.headers['Content-Type'], 'text/csv') - - # Validate CSV output against expected results - csv_file = StringIO(csv_response.content.decode('utf8')) - reader = csv.DictReader(csv_file) - for index, record in enumerate(reader): - expected_email, expected_organization_name = expected_results[index] - self.assertEqual(record['email'], expected_email) - self.assertEqual(record['organization_name'], expected_organization_name) - self.assertEqual(index + 1, len(expected_results)) - - # Get JSON export output - json_response: HttpResponse = self.get( - 'user_management_manage_organization_user_export_user_list', - request_kwargs={'data': {'format': 'json'}}, - user=self.admin_user, - ) - self.assertEqual(json_response.headers['Content-Type'], 'application/json') - - # Validate JSON output against expected results - reader = json.loads(json_response.content) - for index, record in enumerate(reader): - expected_email, expected_organization_name = expected_results[index] - self.assertEqual(record['email'], expected_email) - self.assertEqual(record['organization_name'], expected_organization_name) - self.assertEqual(index + 1, len(expected_results)) - - def test_sponsored_user_export_user_list(self): - expected_results = [ - ('another_inactive_sponsored_user@example.com', 'inactive'), - ('another_sponsored_user@example.com', 'active'), - ('inactive_sponsored_user@example.com', 'inactive'), - ('test_sponsored_user@example.com', 'active'), - ] - - # Get CSV export output - csv_response: HttpResponse = self.get( - 'user_management_manage_sponsored_user_export_user_list', - request_kwargs={'data': {'format': 'csv'}}, - user=self.admin_user, - ) - self.assertEqual(csv_response.headers['Content-Type'], 'text/csv') - - # Validate CSV output against expected results - csv_file = StringIO(csv_response.content.decode('utf8')) - reader = csv.DictReader(csv_file) - for index, record in enumerate(reader): - expected_email, expected_sponsorship_status = expected_results[index] - self.assertEqual(record['email'], expected_email) - self.assertEqual(record['sponsorship_status'], expected_sponsorship_status) - self.assertEqual(index + 1, len(expected_results)) - - # Get JSON export output - json_response: HttpResponse = self.get( - 'user_management_manage_sponsored_user_export_user_list', - request_kwargs={'data': {'format': 'json'}}, - user=self.admin_user, - ) - self.assertEqual(json_response.headers['Content-Type'], 'application/json') - - # Validate JSON output against expected results - reader = json.loads(json_response.content) - for index, record in enumerate(reader): - expected_email, expected_sponsorship_status = expected_results[index] - self.assertEqual(record['email'], expected_email) - self.assertEqual(record['sponsorship_status'], expected_sponsorship_status) - self.assertEqual(index + 1, len(expected_results)) def test_sponsored_user_list_filters(self): # test assumptions: four users, with five sponsorships between them From 6119f8e9a373b0e06abda3ed2834b16f00127c29 Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Tue, 7 Jan 2025 17:38:27 -0500 Subject: [PATCH 07/20] Lint. --- perma_web/perma/tests/test_views_user_management.py | 1 - 1 file changed, 1 deletion(-) diff --git a/perma_web/perma/tests/test_views_user_management.py b/perma_web/perma/tests/test_views_user_management.py index f0582eea1..2aa751519 100644 --- a/perma_web/perma/tests/test_views_user_management.py +++ b/perma_web/perma/tests/test_views_user_management.py @@ -8,7 +8,6 @@ import re from bs4 import BeautifulSoup -from django.http import HttpResponse, JsonResponse from django.urls import reverse from django.core import mail from django.core.files.uploadedfile import SimpleUploadedFile From 31f34997434dbf8844737ff40f977e7360850685 Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Fri, 10 Jan 2025 09:33:47 -0500 Subject: [PATCH 08/20] Admins can create, delete, deactivate, and reactivate users. --- perma_web/conftest.py | 120 +++++++++---- .../perma/tests/test_views_user_management.py | 165 +++++++++++++----- 2 files changed, 211 insertions(+), 74 deletions(-) diff --git a/perma_web/conftest.py b/perma_web/conftest.py index e06cbc78d..a86a62650 100644 --- a/perma_web/conftest.py +++ b/perma_web/conftest.py @@ -197,7 +197,9 @@ def f(page, user): from dateutil.relativedelta import relativedelta from django.utils import timezone -from perma.models import Registrar, Organization, LinkUser, Link, CaptureJob, Capture, Sponsorship, Folder +from perma.models import (Registrar, Organization, LinkUser, UserOrganizationAffiliation, + Link, CaptureJob, Capture, Sponsorship, Folder +) from perma.utils import pp_date_from_post @@ -348,8 +350,22 @@ class PayingRegistrarUserFactory(LinkUserFactory): registrar = factory.SubFactory(PayingRegistrarFactory) -# SponsorshipFactory has to come after RegistrarUserFactory and LinkUserFactory, -# and before SponsoredUserFactory +@register_factory +class UnconfirmedRegistrarUserFactory( + UnactivatedUserFactory, + RegistrarUserFactory + ): + pass + + +@register_factory +class DeactivatedRegistrarUserFactory( + DeactivatedUserFactory, + RegistrarUserFactory + ): + pass + + @register_factory class SponsorshipFactory(DjangoModelFactory): class Meta: @@ -373,6 +389,21 @@ class SponsoredUserFactory(LinkUserFactory): ) +@register_factory +class UnconfirmedSponsoredUserFactory( + UnactivatedUserFactory, + SponsoredUserFactory + ): + pass + +@register_factory +class DeactivatedSponsoredUserFactory( + DeactivatedUserFactory, + SponsoredUserFactory + ): + pass + + @register_factory class NonpayingUserFactory(LinkUserFactory): nonpaying = True @@ -388,6 +419,41 @@ class PayingUserFactory(LinkUserFactory): in_trial = False +@register_factory +class UserOrganizationAffiliationFactory(DjangoModelFactory): + class Meta: + model = UserOrganizationAffiliation + + user = factory.SubFactory(LinkUserFactory) + organization = factory.SubFactory(OrganizationFactory) + + +@register_factory +class OrgUserFactory(LinkUserFactory): + + organizations = factory.RelatedFactoryList( + UserOrganizationAffiliationFactory, + size=1, + factory_related_name='user' + ) + + +@register_factory +class UnconfirmedOrgUserFactory( + UnactivatedUserFactory, + OrgUserFactory + ): + pass + + +@register_factory +class DeactivatedOrgUserFactory( + DeactivatedUserFactory, + OrgUserFactory + ): + pass + + @register_factory class CaptureJobFactory(DjangoModelFactory): class Meta: @@ -533,46 +599,38 @@ def generic(self, *args, **kwargs): @pytest.fixture -def user_data(): - first_name = FAKE.first_name() - last_name = FAKE.last_name() - email = f"{first_name}_{last_name}@example.com" - return { - "first_name": first_name, - "last_name": last_name, - "email": email, - "normalized_email": email.lower() - } - - -@pytest.fixture -def admin_user(link_user_factory): - return link_user_factory(is_staff=True) +def user_data_factory(): + def f(): + first_name = FAKE.first_name() + last_name = FAKE.last_name() + email = f"{first_name}_{last_name}@example.com" + return { + "first_name": first_name, + "last_name": last_name, + "email": email, + "normalized_email": email.lower() + } + return f @pytest.fixture -def org_user_factory(link_user_factory, organization_factory): - def f(orgs=None): - link_user = link_user_factory() - if orgs: - link_user.organizations.set(orgs) - else: - link_user.organizations.add(organization_factory()) - return link_user - return f +def user_data(user_data_factory): + return user_data_factory() @pytest.fixture -def org_user(org_user_factory): - return org_user_factory() +def admin_user(link_user_factory): + return link_user_factory(is_staff=True) @pytest.fixture -def multi_registrar_org_user(org_user_factory, organization_factory): +def multi_registrar_org_user(link_user_factory, organization_factory): first = organization_factory() second = organization_factory() assert first.registrar != second.registrar - return org_user_factory(orgs=[first, second]) + user = link_user_factory() + user.organizations.set([first, second]) + return user @pytest.fixture diff --git a/perma_web/perma/tests/test_views_user_management.py b/perma_web/perma/tests/test_views_user_management.py index 2aa751519..1313f921f 100644 --- a/perma_web/perma/tests/test_views_user_management.py +++ b/perma_web/perma/tests/test_views_user_management.py @@ -361,6 +361,128 @@ def test_sponsored_user_export_user_list(flush_db, export_format, mime_type, cli assert index + 1 == len(sponsored_user_list) +@pytest.mark.parametrize( + "view_name,form_field", + [ + ('user', ''), + ('registrar_user', 'a-registrar'), + ('organization_user', 'a-organizations'), + ('sponsored_user', 'a-sponsoring_registrars') + ] +) +def test_admin_can_create_users(view_name, form_field, client, admin_user, registrar, user_data_factory): + # Setup + client.force_login(admin_user) + user_data = user_data_factory() + common_fields = { + 'a-first_name': user_data['first_name'], + 'a-last_name': user_data['last_name'], + 'a-e-address': user_data['email'] + } + match view_name: + case 'registrar_user': + view_specific_fields = {form_field: registrar.id} + case 'organization_user': + view_specific_fields = {form_field: registrar.organizations.first().id} + case 'sponsored_user': + view_specific_fields = {form_field: registrar.id} + case _: + view_specific_fields = {} + + # Create the user + submit_form( + client, + data={**common_fields, **view_specific_fields}, + view_name='user_management_' + view_name + '_add_user', + success_url=reverse('user_management_manage_' + view_name), + success_query=LinkUser.objects.filter( + email=user_data['normalized_email'], + raw_email=user_data['email'] + ) + ) + + +def attempt_deletion(user_type, view_name, request, client, admin_user, deactivate=False): + client.force_login(admin_user) + user = request.getfixturevalue(user_type) + if deactivate: + assert user.is_active + + submit_form( + client, + url=reverse( + 'user_management_manage_single_' + view_name + '_delete', + args=[user.id] + ), + success_url=reverse('user_management_manage_' + view_name) + ) + + if deactivate: + user.refresh_from_db() + assert not user.is_active + else: + with pytest.raises(LinkUser.DoesNotExist): + user.refresh_from_db() + + + +@pytest.mark.parametrize( + "user_type, view_name", + [ + ('link_user', 'user'), + ('registrar_user', 'registrar_user'), + ('org_user', 'organization_user'), + ('sponsored_user', 'sponsored_user'), + ] +) +def test_admin_can_deactivate_confirmed_users(user_type, view_name, request, client, admin_user): + # If you attempt to delete a user where is_confirmed is True, + # they are not deleted, they are deactivated + attempt_deletion(user_type, view_name, request, client, admin_user, deactivate=True) + + +@pytest.mark.parametrize( + "user_type, view_name", + [ + ('unactivated_user', 'user'), + ('unconfirmed_registrar_user', 'registrar_user'), + ('unconfirmed_org_user', 'organization_user'), + ('unconfirmed_sponsored_user', 'sponsored_user'), + ] +) +def test_admin_can_delete_unconfirmed_users(user_type, view_name, request, client, admin_user): + # If you attempt to delete a user where is_confirmed is False, + # they are deleted + attempt_deletion(user_type, view_name, request, client, admin_user) + + +@pytest.mark.parametrize( + "user_type, view_name", + [ + ('deactivated_user', 'user'), + ('deactivated_registrar_user', 'registrar_user'), + ('deactivated_org_user', 'organization_user'), + ('deactivated_sponsored_user', 'sponsored_user'), + ] +) +def test_admin_can_reactivate_deactivated_users(user_type, view_name, request, client, admin_user): + client.force_login(admin_user) + user = request.getfixturevalue(user_type) + assert not user.is_active + + submit_form( + client, + url=reverse( + 'user_management_manage_single_' + view_name + '_reactivate', + args=[user.id] + ), + success_url=reverse('user_management_manage_' + view_name) + ) + + user.refresh_from_db() + assert user.is_active + + class UserManagementViewsTestCase(PermaTestCase): @classmethod @@ -686,49 +808,6 @@ def test_user_list_filters(self): # status filter tested in test_registrar_user_list_filters - def test_create_and_delete_user(self): - self.log_in_user(self.admin_user) - - base_user = { - 'a-first_name':'First', - 'a-last_name':'Last', - } - email = self.randomize_capitalization('test_views_test@test.com') - normalized_email = email.lower() - - for view_name, form_extras in [ - ['registrar_user', {'a-registrar': 1}], - ['user', {}], - ['organization_user', {'a-organizations': 1}], - ['sponsored_user', {'a-sponsoring_registrars': 1}], - ]: - # create user - email += '1' - normalized_email += '1' - self.submit_form('user_management_' + view_name + '_add_user', - data=dict(list(base_user.items()) + list(form_extras.items()) + [['a-e-address', email]]), - success_url=reverse('user_management_manage_' + view_name), - success_query=LinkUser.objects.filter(email=normalized_email, raw_email=email)) - new_user = LinkUser.objects.get(email=normalized_email) - - # delete user (deactivate) - new_user.is_confirmed = True - new_user.save() - self.submit_form('user_management_manage_single_' + view_name + '_delete', - reverse_kwargs={'args': [new_user.pk]}, - success_url=reverse('user_management_manage_' + view_name)) - - # reactivate user - self.submit_form('user_management_manage_single_' + view_name + '_reactivate', - reverse_kwargs={'args': [new_user.pk]}, - success_url=reverse('user_management_manage_' + view_name)) - - # delete user (really delete) - new_user.is_confirmed = False - new_user.save() - self.submit_form('user_management_manage_single_' + view_name + '_delete', - reverse_kwargs={'args': [new_user.pk]}, - success_url=reverse('user_management_manage_' + view_name)) ### ADDING NEW USERS TO ORGANIZATIONS ### From 5c40eb2a8281c331b3f304b47f20c8a04d8aac38 Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Fri, 10 Jan 2025 09:41:17 -0500 Subject: [PATCH 09/20] Re-order tests; put export lists at the end. --- .../perma/tests/test_views_user_management.py | 243 +++++++++--------- 1 file changed, 126 insertions(+), 117 deletions(-) diff --git a/perma_web/perma/tests/test_views_user_management.py b/perma_web/perma/tests/test_views_user_management.py index 1313f921f..74186af30 100644 --- a/perma_web/perma/tests/test_views_user_management.py +++ b/perma_web/perma/tests/test_views_user_management.py @@ -240,7 +240,132 @@ def test_even_admin_cannot_delete_organization_with_links(client, admin_user, or ### -### EXPORT LISTS +### USER A/E/D VIEWS ### +### + +@pytest.mark.parametrize( + "view_name,form_field", + [ + ('user', ''), + ('registrar_user', 'a-registrar'), + ('organization_user', 'a-organizations'), + ('sponsored_user', 'a-sponsoring_registrars') + ] +) +def test_admin_can_create_users(view_name, form_field, client, admin_user, registrar, user_data_factory): + # Setup + client.force_login(admin_user) + user_data = user_data_factory() + common_fields = { + 'a-first_name': user_data['first_name'], + 'a-last_name': user_data['last_name'], + 'a-e-address': user_data['email'] + } + match view_name: + case 'registrar_user': + view_specific_fields = {form_field: registrar.id} + case 'organization_user': + view_specific_fields = {form_field: registrar.organizations.first().id} + case 'sponsored_user': + view_specific_fields = {form_field: registrar.id} + case _: + view_specific_fields = {} + + # Create the user + submit_form( + client, + data={**common_fields, **view_specific_fields}, + view_name='user_management_' + view_name + '_add_user', + success_url=reverse('user_management_manage_' + view_name), + success_query=LinkUser.objects.filter( + email=user_data['normalized_email'], + raw_email=user_data['email'] + ) + ) + + +def attempt_deletion(user_type, view_name, request, client, admin_user, deactivate=False): + client.force_login(admin_user) + user = request.getfixturevalue(user_type) + if deactivate: + assert user.is_active + + submit_form( + client, + url=reverse( + 'user_management_manage_single_' + view_name + '_delete', + args=[user.id] + ), + success_url=reverse('user_management_manage_' + view_name) + ) + + if deactivate: + user.refresh_from_db() + assert not user.is_active + else: + with pytest.raises(LinkUser.DoesNotExist): + user.refresh_from_db() + + +@pytest.mark.parametrize( + "user_type, view_name", + [ + ('link_user', 'user'), + ('registrar_user', 'registrar_user'), + ('org_user', 'organization_user'), + ('sponsored_user', 'sponsored_user'), + ] +) +def test_admin_can_deactivate_confirmed_users(user_type, view_name, request, client, admin_user): + # If you attempt to delete a user where is_confirmed is True, + # they are not deleted, they are deactivated + attempt_deletion(user_type, view_name, request, client, admin_user, deactivate=True) + + +@pytest.mark.parametrize( + "user_type, view_name", + [ + ('unactivated_user', 'user'), + ('unconfirmed_registrar_user', 'registrar_user'), + ('unconfirmed_org_user', 'organization_user'), + ('unconfirmed_sponsored_user', 'sponsored_user'), + ] +) +def test_admin_can_delete_unconfirmed_users(user_type, view_name, request, client, admin_user): + # If you attempt to delete a user where is_confirmed is False, + # they are deleted + attempt_deletion(user_type, view_name, request, client, admin_user) + + +@pytest.mark.parametrize( + "user_type, view_name", + [ + ('deactivated_user', 'user'), + ('deactivated_registrar_user', 'registrar_user'), + ('deactivated_org_user', 'organization_user'), + ('deactivated_sponsored_user', 'sponsored_user'), + ] +) +def test_admin_can_reactivate_deactivated_users(user_type, view_name, request, client, admin_user): + client.force_login(admin_user) + user = request.getfixturevalue(user_type) + assert not user.is_active + + submit_form( + client, + url=reverse( + 'user_management_manage_single_' + view_name + '_reactivate', + args=[user.id] + ), + success_url=reverse('user_management_manage_' + view_name) + ) + + user.refresh_from_db() + assert user.is_active + + +### +### EXPORT USER LISTS ### @@ -361,126 +486,10 @@ def test_sponsored_user_export_user_list(flush_db, export_format, mime_type, cli assert index + 1 == len(sponsored_user_list) -@pytest.mark.parametrize( - "view_name,form_field", - [ - ('user', ''), - ('registrar_user', 'a-registrar'), - ('organization_user', 'a-organizations'), - ('sponsored_user', 'a-sponsoring_registrars') - ] -) -def test_admin_can_create_users(view_name, form_field, client, admin_user, registrar, user_data_factory): - # Setup - client.force_login(admin_user) - user_data = user_data_factory() - common_fields = { - 'a-first_name': user_data['first_name'], - 'a-last_name': user_data['last_name'], - 'a-e-address': user_data['email'] - } - match view_name: - case 'registrar_user': - view_specific_fields = {form_field: registrar.id} - case 'organization_user': - view_specific_fields = {form_field: registrar.organizations.first().id} - case 'sponsored_user': - view_specific_fields = {form_field: registrar.id} - case _: - view_specific_fields = {} - - # Create the user - submit_form( - client, - data={**common_fields, **view_specific_fields}, - view_name='user_management_' + view_name + '_add_user', - success_url=reverse('user_management_manage_' + view_name), - success_query=LinkUser.objects.filter( - email=user_data['normalized_email'], - raw_email=user_data['email'] - ) - ) - - -def attempt_deletion(user_type, view_name, request, client, admin_user, deactivate=False): - client.force_login(admin_user) - user = request.getfixturevalue(user_type) - if deactivate: - assert user.is_active - - submit_form( - client, - url=reverse( - 'user_management_manage_single_' + view_name + '_delete', - args=[user.id] - ), - success_url=reverse('user_management_manage_' + view_name) - ) - - if deactivate: - user.refresh_from_db() - assert not user.is_active - else: - with pytest.raises(LinkUser.DoesNotExist): - user.refresh_from_db() - - - -@pytest.mark.parametrize( - "user_type, view_name", - [ - ('link_user', 'user'), - ('registrar_user', 'registrar_user'), - ('org_user', 'organization_user'), - ('sponsored_user', 'sponsored_user'), - ] -) -def test_admin_can_deactivate_confirmed_users(user_type, view_name, request, client, admin_user): - # If you attempt to delete a user where is_confirmed is True, - # they are not deleted, they are deactivated - attempt_deletion(user_type, view_name, request, client, admin_user, deactivate=True) - - -@pytest.mark.parametrize( - "user_type, view_name", - [ - ('unactivated_user', 'user'), - ('unconfirmed_registrar_user', 'registrar_user'), - ('unconfirmed_org_user', 'organization_user'), - ('unconfirmed_sponsored_user', 'sponsored_user'), - ] -) -def test_admin_can_delete_unconfirmed_users(user_type, view_name, request, client, admin_user): - # If you attempt to delete a user where is_confirmed is False, - # they are deleted - attempt_deletion(user_type, view_name, request, client, admin_user) -@pytest.mark.parametrize( - "user_type, view_name", - [ - ('deactivated_user', 'user'), - ('deactivated_registrar_user', 'registrar_user'), - ('deactivated_org_user', 'organization_user'), - ('deactivated_sponsored_user', 'sponsored_user'), - ] -) -def test_admin_can_reactivate_deactivated_users(user_type, view_name, request, client, admin_user): - client.force_login(admin_user) - user = request.getfixturevalue(user_type) - assert not user.is_active - submit_form( - client, - url=reverse( - 'user_management_manage_single_' + view_name + '_reactivate', - args=[user.id] - ), - success_url=reverse('user_management_manage_' + view_name) - ) - user.refresh_from_db() - assert user.is_active class UserManagementViewsTestCase(PermaTestCase): From e3c87f136fa8a3418b76297d6e65d99bc2246f1d Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Thu, 16 Jan 2025 11:27:47 -0500 Subject: [PATCH 10/20] Convert tests for adding and removing org users. --- .../perma/tests/test_views_user_management.py | 506 ++++++++++-------- .../perma/tests/test_views_user_settings.py | 45 ++ 2 files changed, 315 insertions(+), 236 deletions(-) diff --git a/perma_web/perma/tests/test_views_user_management.py b/perma_web/perma/tests/test_views_user_management.py index 74186af30..dd175b887 100644 --- a/perma_web/perma/tests/test_views_user_management.py +++ b/perma_web/perma/tests/test_views_user_management.py @@ -20,7 +20,7 @@ from perma.tests.utils import PermaTestCase from perma.forms import MultipleUsersFormWithOrganization -from conftest import submit_form +from conftest import submit_form, randomize_capitalization ### @@ -364,6 +364,275 @@ def test_admin_can_reactivate_deactivated_users(user_type, view_name, request, c assert user.is_active +### +### ADDING USERS TO ORGANIZATIONS ### +### + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + "admin_user" + ] +) +def test_can_add_new_user_to_org(user_type, request, client, user_data): + user = request.getfixturevalue(user_type) + client.force_login(user) + + match user_type: + case "org_user": + org = user.organizations.first() + case "registrar_user": + org = user.registrar.organizations.first() + case "admin_user": + org = request.getfixturevalue("organization") + + submit_form( + client, + url=f"{reverse('user_management_organization_user_add_user')}?email={user_data['email']}", + data={ + "a-organizations": org.id, + "a-first_name": user_data['first_name'], + "a-last_name": user_data['last_name'], + "a-e-address": user_data['email'], + }, + success_url=reverse("user_management_manage_organization_user"), + success_query=LinkUser.objects.filter( + email=user_data['normalized_email'], + raw_email=user_data['email'], + organizations=org + ).exists() + ) + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user" + ] +) +def test_cannot_add_new_user_to_inaccessible_org(user_type, request, client, user_data, organization_factory): + user = request.getfixturevalue(user_type) + client.force_login(user) + unrelated_org = organization_factory() + + submit_form( + client, + url=f"{reverse('user_management_organization_user_add_user')}?email={user_data['email']}", + data={ + "a-organizations": unrelated_org.id, + "a-first_name": user_data['first_name'], + "a-last_name": user_data['last_name'], + "a-e-address": user_data['email'], + }, + error_keys=['organizations'] + ) + assert not LinkUser.objects.filter(email__iexact=user_data['email']).exists() + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + "admin_user" + ] +) +def test_can_add_existing_user_to_org(user_type, request, client, link_user): + user = request.getfixturevalue(user_type) + client.force_login(user) + + match user_type: + case "org_user": + org = user.organizations.first() + case "registrar_user": + org = user.registrar.organizations.first() + case "admin_user": + org = request.getfixturevalue("organization") + + scrambled_email = randomize_capitalization(link_user.email) + submit_form( + client, + url=f"{reverse('user_management_organization_user_add_user')}?email={scrambled_email}", + data={ + "a-organizations": org.id + }, + success_url=reverse("user_management_manage_organization_user"), + success_query=link_user.organizations.filter(id=org.id) + ) + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user" + ] +) +def test_cannot_add_existing_user_to_inaccessible_org(user_type, request, client, link_user, organization_factory): + user = request.getfixturevalue(user_type) + client.force_login(user) + unrelated_org = organization_factory() + + scrambled_email = randomize_capitalization(link_user.email) + submit_form( + client, + url=f"{reverse('user_management_organization_user_add_user')}?email={scrambled_email}", + data={ + "a-organizations": unrelated_org.id + }, + error_keys=['organizations'] + ) + assert not link_user.organizations.filter(id=unrelated_org.id).exists() + + +def test_cannot_add_admin_user_to_org(client, admin_user, organization): + client.force_login(admin_user) + + scrambled_email = randomize_capitalization(admin_user.email) + response = submit_form( + client, + url=f"{reverse('user_management_organization_user_add_user')}?email={scrambled_email}", + data={ + "a-organizations": organization.id + } + ) + + assert b"is an admin user" in response.content + assert not admin_user.organizations.exists() + + +def test_cannot_add_registrar_user_to_org(client, admin_user, registrar_user): + client.force_login(admin_user) + org = registrar_user.registrar.organizations.first() + + scrambled_email = randomize_capitalization(registrar_user.email) + response = submit_form( + client, + url=f"{reverse('user_management_organization_user_add_user')}?email={scrambled_email}", + data={ + "a-organizations": org.id + } + ) + assert b"is already a registrar user"in response.content + assert not registrar_user.organizations.exists() + + +### +# REMOVING USERS FROM ORGANIZATIONS ### +### + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + "admin_user" + ] +) +def test_can_visit_org_user_edit_page(user_type, request, client, link_user): + user = request.getfixturevalue(user_type) + client.force_login(user) + + match user_type: + case "org_user": + org = user.organizations.first() + case "registrar_user": + org = user.registrar.organizations.first() + case "admin_user": + org = request.getfixturevalue("organization") + link_user.organizations.set([org]) + + response = client.get( + reverse('user_management_manage_single_organization_user', args=[link_user.id]), + secure=True + ) + assert response.status_code == 200 + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + ] +) +def test_cannot_visit_unrelated_org_user_edit_page(user_type, request, client, org_user_factory): + user = request.getfixturevalue(user_type) + client.force_login(user) + unrelated_org_user = org_user_factory() + + response = client.get( + reverse('user_management_manage_single_organization_user', args=[unrelated_org_user.id]), + secure=True + ) + assert response.status_code == 403 + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + "admin_user" + ] +) +def test_can_remove_user_from_organization(user_type, request, client, link_user): + user = request.getfixturevalue(user_type) + client.force_login(user) + + match user_type: + case "org_user": + org = user.organizations.first() + case "registrar_user": + org = user.registrar.organizations.first() + case "admin_user": + org = request.getfixturevalue("organization") + link_user.organizations.set([org]) + + assert link_user.organizations.filter(id=org.id).exists() + submit_form( + client, + url=reverse('user_management_manage_single_organization_user_remove', args=[link_user.id]), + data={'affiliation': link_user.userorganizationaffiliation_set.first().id}, + success_url=reverse('user_management_manage_organization_user') + ) + assert not link_user.organizations.filter(id=org.id).exists() + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + ] +) +def test_cannot_remove_user_from_unrelated_organization(user_type, request, client, org_user_factory): + user = request.getfixturevalue(user_type) + client.force_login(user) + unrelated_org_user = org_user_factory() + + submit_form( + client, + url=reverse('user_management_manage_single_organization_user_remove', args=[unrelated_org_user.id]), + data={'affiliation': unrelated_org_user.userorganizationaffiliation_set.first().id}, + require_status_code=404 + ) + + +def test_can_remove_self_from_organization(client, org_user): + client.force_login(org_user) + submit_form( + client, + url=reverse('user_management_manage_single_organization_user_remove', args=[org_user.id]), + data={'affiliation': org_user.userorganizationaffiliation_set.first().id}, + success_url=reverse('create_link') + ) + assert not org_user.organizations.exists() + + ### ### EXPORT USER LISTS ### @@ -486,12 +755,6 @@ def test_sponsored_user_export_user_list(flush_db, export_format, mime_type, cli assert index + 1 == len(sponsored_user_list) - - - - - - class UserManagementViewsTestCase(PermaTestCase): @classmethod @@ -818,24 +1081,6 @@ def test_user_list_filters(self): # status filter tested in test_registrar_user_list_filters - ### ADDING NEW USERS TO ORGANIZATIONS ### - - def add_org_user(self): - email = self.randomize_capitalization('doesnotexist@example.com') - normalized_email = email.lower() - self.submit_form('user_management_organization_user_add_user', - data={'a-organizations': self.organization.pk, - 'a-first_name': 'First', - 'a-last_name': 'Last', - 'a-e-address': email}, - query_params={'email': email}, - success_url=reverse('user_management_manage_organization_user'), - success_query=LinkUser.objects.filter( - email=normalized_email, - raw_email=email, - organizations=self.organization - ).exists()) - def test_add_multiple_org_users_via_csv(self): def create_csv_file(filename, content): return SimpleUploadedFile(filename, content.encode('utf-8'), content_type='text/csv') @@ -910,217 +1155,6 @@ def initialize_form(csv_file, data=None): self.assertEqual(len(form5.ineligible_users), 1) self.assertEqual("johndoe@example.com", next(iter(form5.ineligible_users))) - def test_admin_user_can_add_new_user_to_org(self): - self.log_in_user(self.admin_user) - self.add_org_user() - - def test_registrar_user_can_add_new_user_to_org(self): - self.log_in_user(self.registrar_user) - self.add_org_user() - - def test_org_user_can_add_new_user_to_org(self): - self.log_in_user(self.organization_user) - self.add_org_user() - - def test_registrar_user_cannot_add_new_user_to_inaccessible_org(self): - self.log_in_user(self.registrar_user) - self.submit_form('user_management_organization_user_add_user', - data={'a-organizations': self.unrelated_organization.pk, - 'a-first_name': 'First', - 'a-last_name': 'Last', - 'a-e-address': 'doesnotexist@example.com'}, - query_params={'email': 'doesnotexist@example.com'}, - error_keys=['organizations']) - self.assertFalse(LinkUser.objects.filter(email='doesnotexist@example.com', - organizations=self.unrelated_organization).exists()) - - def test_org_user_cannot_add_new_user_to_inaccessible_org(self): - self.log_in_user(self.organization_user) - self.submit_form('user_management_organization_user_add_user', - data={'a-organizations': self.unrelated_organization.pk, - 'a-first_name': 'First', - 'a-last_name': 'Last', - 'a-e-address': 'doesnotexist@example.com'}, - query_params={'email': 'doesnotexist@example.com'}, - error_keys=['organizations']) - self.assertFalse(LinkUser.objects.filter(email='doesnotexist@example.com', - organizations=self.unrelated_organization).exists()) - - ### ADDING EXISTING USERS TO ORGANIZATIONS ### - - def add_org_users(self): - # submit email with the same capitalization - self.submit_form('user_management_organization_user_add_user', - data={'a-organizations': self.organization.pk}, - query_params={'email': self.regular_user.email}, - success_url=reverse('user_management_manage_organization_user'), - success_query=self.regular_user.organizations.filter(pk=self.organization.pk)) - - # submit email with a different capitalization - scrambled_email = self.randomize_capitalization(self.another_regular_user.email) - self.submit_form('user_management_organization_user_add_user', - data={'a-organizations': self.organization.pk}, - query_params={'email': scrambled_email}, - success_url=reverse('user_management_manage_organization_user'), - success_query=self.another_regular_user.organizations.filter(pk=self.organization.pk)) - - def test_admin_user_can_add_existing_user_to_org(self): - self.log_in_user(self.admin_user) - self.add_org_users() - - def test_registrar_user_can_add_existing_user_to_org(self): - self.log_in_user(self.registrar_user) - self.add_org_users() - - def test_org_user_can_add_existing_user_to_org(self): - self.log_in_user(self.organization_user) - self.add_org_users() - - def test_registrar_user_cannot_add_existing_user_to_inaccessible_org(self): - self.log_in_user(self.registrar_user) - self.submit_form('user_management_organization_user_add_user', - data={'a-organizations': self.unrelated_organization.pk}, - query_params={'email': self.regular_user.email}, - error_keys=['organizations']) - self.assertFalse(self.regular_user.organizations.filter(pk=self.unrelated_organization.pk).exists()) - - def test_org_user_cannot_add_existing_user_to_inaccessible_org(self): - self.log_in_user(self.organization_user) - self.submit_form('user_management_organization_user_add_user', - data={'a-organizations': self.another_organization.pk}, - query_params={'email': self.regular_user.email}, - error_keys=['organizations']) - self.assertFalse(self.regular_user.organizations.filter(pk=self.another_organization.pk).exists()) - - def test_cannot_add_admin_user_to_org(self): - self.log_in_user(self.organization_user) - resp = self.submit_form('user_management_organization_user_add_user', - data={'a-organizations': self.organization.pk}, - query_params={'email': self.admin_user.email}) - self.assertIn(b"is an admin user", resp.content) - self.assertFalse(self.admin_user.organizations.exists()) - - def test_cannot_add_registrar_user_to_org(self): - self.log_in_user(self.organization_user) - resp = self.submit_form('user_management_organization_user_add_user', - data={'a-organizations': self.organization.pk}, - query_params={'email': self.registrar_user.email}) - self.assertIn(b"is already a registrar user", resp.content) - self.assertFalse(self.registrar_user.organizations.exists()) - - ### VOLUNTARILY LEAVING ORGANIZATIONS ### - - def test_org_user_can_leave_org(self): - u = LinkUser.objects.get(email='test_another_library_org_user@example.com') - orgs = u.organizations.all() - - # check assumptions - self.assertEqual(len(orgs), 2) - - # 404 if tries to leave non-existent org - self.submit_form('user_management_organization_user_leave_organization', - user=u, - data={}, - reverse_kwargs={'args': [999]}, - require_status_code=404) - - # returns to affiliations page if still a member of at least one org - self.submit_form('user_management_organization_user_leave_organization', - user=u, - data={}, - reverse_kwargs={'args': [orgs[0].pk]}, - success_url=reverse('settings_affiliations')) - - # returns to create/manage page if no longer a member of any orgs - self.submit_form('user_management_organization_user_leave_organization', - user=u, - data={}, - reverse_kwargs={'args': [orgs[1].pk]}, - success_url=reverse('create_link')) - - # 404 if tries to leave an org they are not a member of - self.submit_form('user_management_organization_user_leave_organization', - user=u, - data={}, - reverse_kwargs={'args': [orgs[1].pk]}, - require_status_code=404) - - - ### REMOVING USERS FROM ORGANIZATIONS ### - - # Just try to access the page with remove/deactivate links - - def test_registrar_can_edit_org_user(self): - # User from one of registrar's own orgs succeeds - self.log_in_user(self.registrar_user) - self.get('user_management_manage_single_organization_user', - reverse_kwargs={'args': [self.organization_user.pk]}) - # User from another registrar's org fails - self.get('user_management_manage_single_organization_user', - reverse_kwargs={'args': [self.another_unrelated_organization_user.pk]}, - require_status_code=403) - # Repeat with the other registrar, to confirm we're - # getting 404s because of permission reasons, not because the - # test fixtures are broken. - self.log_in_user(self.unrelated_registrar_user) - self.get('user_management_manage_single_organization_user', - reverse_kwargs={'args': [self.organization_user.pk]}, - require_status_code=403) - self.get('user_management_manage_single_organization_user', - reverse_kwargs={'args': [self.another_unrelated_organization_user.pk]}) - - def test_org_can_edit_org_user(self): - # User from own org succeeds - org_one_users = ['test_org_user@example.com', 'test_org_rando_user@example.com'] - org_two_users = ['test_another_library_org_user@example.com', 'test_another_org_user@example.com'] - - self.log_in_user(org_one_users[0]) - self.get('user_management_manage_single_organization_user', - reverse_kwargs={'args': [self.pk_from_email(org_one_users[1])]}) - # User from another org fails - self.get('user_management_manage_single_organization_user', - reverse_kwargs={'args': [self.pk_from_email(org_two_users[0])]}, - require_status_code=403) - - # Repeat with another org - self.log_in_user(org_two_users[1]) - self.get('user_management_manage_single_organization_user', - reverse_kwargs={'args': [self.pk_from_email(org_one_users[1])]}, - require_status_code=403) - self.get('user_management_manage_single_organization_user', - reverse_kwargs={'args': [self.pk_from_email(org_two_users[0])]}) - - # Actually try removing them - - def test_can_remove_user_from_organization(self): - self.log_in_user(self.registrar_user) - self.submit_form('user_management_manage_single_organization_user_remove', - data={'affiliation': self.user_organization_affiliation.pk}, - reverse_kwargs={'args': [self.organization_user.pk]}, - success_url=reverse('user_management_manage_organization_user')) - self.assertFalse(self.organization_user.organizations.filter(pk=self.user_organization_affiliation.pk).exists()) - - def test_registrar_cannot_remove_unrelated_user_from_organization(self): - self.log_in_user(self.registrar_user) - self.submit_form('user_management_manage_single_organization_user_remove', - data={'org': self.unrelated_organization.pk}, - reverse_kwargs={'args': [self.unrelated_organization_user.pk]}, - require_status_code=404) - - def test_org_user_cannot_remove_unrelated_user_from_organization(self): - self.log_in_user(self.organization_user) - self.submit_form('user_management_manage_single_organization_user_remove', - data={'org': self.unrelated_organization.pk}, - reverse_kwargs={'args': [self.unrelated_organization_user.pk]}, - require_status_code=404) - - def test_can_remove_self_from_organization(self): - self.log_in_user(self.organization_user) - self.submit_form('user_management_manage_single_organization_user_remove', - data={'affiliation': self.user_organization_affiliation.pk}, - reverse_kwargs={'args': [self.organization_user.pk]}, - success_url=reverse('create_link')) - self.assertFalse(self.organization_user.organizations.filter(pk=self.user_organization_affiliation.pk).exists()) ### ADDING NEW USERS TO REGISTRARS AS SPONSORED USERS ### diff --git a/perma_web/perma/tests/test_views_user_settings.py b/perma_web/perma/tests/test_views_user_settings.py index e4f52ea2e..dabcebc0d 100644 --- a/perma_web/perma/tests/test_views_user_settings.py +++ b/perma_web/perma/tests/test_views_user_settings.py @@ -646,3 +646,48 @@ def test_affiliations_pending_registrar_user(client, pending_registrar_user): assert len(registrar_settings) == 2 for setting in registrar_settings: assert setting.text.strip() in ["Website", "Email"] + + +# +# Affiliations: org users can voluntarily leave orgs +# (can also "remove" themselves via manage/organization-users) +# + +def test_org_user_can_leave_org(client, org_user): + # returns to create/manage page if no longer a member of any orgs + client.force_login(org_user) + submit_form( + client, + url=reverse('user_management_organization_user_leave_organization', args=[org_user.organizations.first().id]), + success_url=reverse('create_link') + ) + + +def test_multi_org_user_can_leave_single_org(client, multi_registrar_org_user): + # returns to affiliations page if still a member of at least one org + client.force_login(multi_registrar_org_user) + submit_form( + client, + url=reverse('user_management_organization_user_leave_organization', args=[multi_registrar_org_user.organizations.first().id]), + success_url=reverse('settings_affiliations') + ) + + +def test_404_if_org_user_tries_to_leave_unrelated_org(client, org_user, organization_factory): + client.force_login(org_user) + unrelated_org = organization_factory() + + submit_form( + client, + url=reverse('user_management_organization_user_leave_organization', args=[unrelated_org.id]), + require_status_code=404 + ) + + +def test_404_if_org_user_tries_to_leave_nonexistent_org(client, org_user): + client.force_login(org_user) + submit_form( + client, + url=reverse('user_management_organization_user_leave_organization', args=[999]), + require_status_code=404 + ) From d7234bbda21fbb9334af430bfc03c78b80da5838 Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Thu, 16 Jan 2025 15:38:52 -0500 Subject: [PATCH 11/20] Convert tests for managing sponsored users. --- perma_web/conftest.py | 11 + .../perma/tests/test_views_user_management.py | 374 ++++++++++-------- 2 files changed, 219 insertions(+), 166 deletions(-) diff --git a/perma_web/conftest.py b/perma_web/conftest.py index a86a62650..36d00c097 100644 --- a/perma_web/conftest.py +++ b/perma_web/conftest.py @@ -403,6 +403,17 @@ class DeactivatedSponsoredUserFactory( ): pass +@register_factory +class InactiveSponsoredUserFactory(LinkUserFactory): + + sponsorships = factory.RelatedFactoryList( + SponsorshipFactory, + size=1, + factory_related_name='user', + status='inactive' + ) + + @register_factory class NonpayingUserFactory(LinkUserFactory): diff --git a/perma_web/perma/tests/test_views_user_management.py b/perma_web/perma/tests/test_views_user_management.py index dd175b887..ab420200f 100644 --- a/perma_web/perma/tests/test_views_user_management.py +++ b/perma_web/perma/tests/test_views_user_management.py @@ -633,6 +633,214 @@ def test_can_remove_self_from_organization(client, org_user): assert not org_user.organizations.exists() +### +### ADDING SPONSORED USERS +### + +def check_sponsorship_is_set_up_correctly(sponsored_user): + sponsorship = sponsored_user.sponsorships.first() + sponsored_folder = sponsorship.folders.get() + assert sponsorship.status == 'active' + assert sponsored_folder.parent == sponsored_user.sponsored_root_folder + assert not sponsored_folder.read_only + + +@pytest.mark.parametrize( + "user_type", + [ + "registrar_user", + "admin_user" + ] +) +def test_can_add_new_sponsored_user_to_registrar(user_type, request, client, user_data): + user = request.getfixturevalue(user_type) + client.force_login(user) + + match user_type: + case "registrar_user": + registrar = user.registrar + case "admin_user": + registrar = request.getfixturevalue("registrar") + + submit_form( + client, + url=f"{reverse('user_management_sponsored_user_add_user')}?email={user_data['email']}", + data={ + "a-sponsoring_registrars": registrar.id, + "a-first_name": user_data['first_name'], + "a-last_name": user_data['last_name'], + "a-e-address": user_data['email'], + }, + success_url=reverse("user_management_manage_sponsored_user"), + ) + + sponsored_user = LinkUser.objects.get( + email=user_data["normalized_email"], + raw_email=user_data["email"], + sponsoring_registrars=registrar + ) + check_sponsorship_is_set_up_correctly(sponsored_user) + + +def test_cannot_add_sponsored_user_to_inaccessible_registrar(client, user_data, registrar_user, registrar_factory): + client.force_login(registrar_user) + unrelated_registrar = registrar_factory() + + submit_form( + client, + url=f"{reverse('user_management_sponsored_user_add_user')}?email={user_data['email']}", + data={ + "a-sponsoring_registrars": unrelated_registrar.id, + "a-first_name": user_data['first_name'], + "a-last_name": user_data['last_name'], + "a-e-address": user_data['email'], + }, + error_keys=['sponsoring_registrars'] + ) + assert not LinkUser.objects.filter(email__iexact=user_data['email']) + + +@pytest.mark.parametrize( + "user_type", + [ + "registrar_user", + "admin_user" + ] +) +def test_can_add_sponsorship_to_existing_user(user_type, request, client, link_user): + user = request.getfixturevalue(user_type) + client.force_login(user) + + match user_type: + case "registrar_user": + registrar = user.registrar + case "admin_user": + registrar = request.getfixturevalue("registrar") + + scrambled_email = randomize_capitalization(link_user.email) + submit_form( + client, + url=f"{reverse('user_management_sponsored_user_add_user')}?email={scrambled_email}", + data={'a-sponsoring_registrars': registrar.id}, + success_url=reverse('user_management_manage_sponsored_user'), + success_query=link_user.sponsorships.filter(registrar=registrar) + ) + + link_user.refresh_from_db() + check_sponsorship_is_set_up_correctly(link_user) + + +def test_registrar_user_cannot_add_sponsorship_for_other_registrar_to_existing_user(client, registrar_user, registrar_factory, link_user): + client.force_login(registrar_user) + unrelated_registrar = registrar_factory() + + scrambled_email = randomize_capitalization(link_user.email) + submit_form( + client, + url=f"{reverse('user_management_sponsored_user_add_user')}?email={scrambled_email}", + data={ + "a-sponsoring_registrars": unrelated_registrar.id, + }, + error_keys=['sponsoring_registrars'] + ) + assert not link_user.sponsorships.filter(registrar=unrelated_registrar).exists() + + +def test_cannot_create_duplicative_sponsorships(client, admin_user, sponsored_user): + client.force_login(admin_user) + + scrambled_email = randomize_capitalization(sponsored_user.email) + response = submit_form( + client, + url=f"{reverse('user_management_sponsored_user_add_user')}?email={scrambled_email}", + data={'a-sponsoring_registrars': sponsored_user.sponsorships.first().registrar.id} + ) + assert b"Select a valid choice. That choice is not one of the available choices" in response.content + +### +### TOGGLING SPONSORSHIP STATUS ### +### + +@pytest.mark.parametrize( + "user_type", + [ + "registrar_user", + "admin_user" + ] +) +def test_can_deactivate_sponsorship(user_type, request, client, sponsored_user): + sponsorship = sponsored_user.sponsorships.get() + match user_type: + case "registrar_user": + user = sponsorship.registrar.users.first() + case "admin_user": + user = request.getfixturevalue("admin_user") + client.force_login(user) + + submit_form( + client, + url=reverse('user_management_manage_single_sponsored_user_remove', args= [sponsored_user.id, sponsorship.registrar.id]), + success_url=reverse('user_management_manage_single_sponsored_user', args=[sponsored_user.id]) + ) + sponsorship.refresh_from_db() + assert sponsorship.status == 'inactive' + assert all(folder.read_only for folder in sponsorship.folders) + + +@pytest.mark.parametrize( + "user_type", + [ + "registrar_user", + "admin_user" + ] +) +def test_can_reactivate_sponsorship(user_type, request, client, inactive_sponsored_user): + sponsorship = inactive_sponsored_user.sponsorships.get() + match user_type: + case "registrar_user": + user = sponsorship.registrar.users.first() + case "admin_user": + user = request.getfixturevalue("admin_user") + client.force_login(user) + + submit_form( + client, + url=reverse('user_management_manage_single_sponsored_user_readd', args= [inactive_sponsored_user.id, sponsorship.registrar.id]), + success_url=reverse('user_management_manage_single_sponsored_user', args=[inactive_sponsored_user.id]) + ) + sponsorship.refresh_from_db() + assert sponsorship.status == 'active' + assert all(not folder.read_only for folder in sponsorship.folders) + + +def test_registrar_user_cannot_deactivate_active_sponsorship_for_other_registrar(client, sponsored_user, registrar_user): + sponsorship = sponsored_user.sponsorships.get() + assert sponsorship.registrar != registrar_user.registrar + client.force_login(registrar_user) + + submit_form( + client, + url=reverse('user_management_manage_single_sponsored_user_remove', args= [sponsored_user.id, sponsorship.registrar.id]), + require_status_code=404 + ) + sponsorship.refresh_from_db() + assert sponsorship.status == 'active' + + +def test_registrar_user_cannot_reactivate_inactive_sponsorship_for_other_registrar(client, inactive_sponsored_user, registrar_user): + sponsorship = inactive_sponsored_user.sponsorships.get() + assert sponsorship.registrar != registrar_user.registrar + client.force_login(registrar_user) + + submit_form( + client, + url=reverse('user_management_manage_single_sponsored_user_readd', args= [inactive_sponsored_user.id, sponsorship.registrar.id]), + require_status_code=404 + ) + sponsorship.refresh_from_db() + assert sponsorship.status == 'inactive' + + ### ### EXPORT USER LISTS ### @@ -1156,172 +1364,6 @@ def initialize_form(csv_file, data=None): self.assertEqual("johndoe@example.com", next(iter(form5.ineligible_users))) - ### ADDING NEW USERS TO REGISTRARS AS SPONSORED USERS ### - - def test_admin_user_can_add_new_sponsored_user_to_registrar(self): - address = self.randomize_capitalization('doesnotexist@example.com') - normalized_address = address.lower() - self.log_in_user(self.admin_user) - self.submit_form('user_management_sponsored_user_add_user', - data={'a-sponsoring_registrars': self.registrar.pk, - 'a-first_name': 'First', - 'a-last_name': 'Last', - 'a-e-address': address}, - query_params={'email': address}, - success_url=reverse('user_management_manage_sponsored_user')) - - # Check that everything is set up correctly (we'll do this once, here, and not repeat in other tests) - user = LinkUser.objects.get( - email=normalized_address, - raw_email=address, - sponsoring_registrars=self.registrar - ) - sponsorship = user.sponsorships.first() - sponsored_folder = sponsorship.folders.get() - self.assertEqual(sponsorship.status, 'active') - self.assertEqual(sponsored_folder.parent, user.sponsored_root_folder) - self.assertFalse(sponsored_folder.read_only) - - # Try to add the same person again; should fail - scrambled_email = self.randomize_capitalization(address) - response = self.submit_form('user_management_sponsored_user_add_user', - data={'a-sponsoring_registrars': self.registrar.pk, - 'a-first_name': 'First', - 'a-last_name': 'Last', - 'a-e-address': scrambled_email}, - query_params={'email': scrambled_email}).content - self.assertIn(bytes("Select a valid choice. That choice is not one of the available choices", 'utf-8'), response) - - def test_registrar_user_can_add_new_sponsored_user_to_registrar(self): - address = self.randomize_capitalization('doesnotexist@example.com') - normalized_address = address.lower() - self.log_in_user(self.registrar_user) - self.submit_form('user_management_sponsored_user_add_user', - data={'a-sponsoring_registrars': self.registrar.pk, - 'a-first_name': 'First', - 'a-last_name': 'Last', - 'a-e-address': address}, - query_params={'email': address}, - success_url=reverse('user_management_manage_sponsored_user'), - success_query=LinkUser.objects.filter( - email=normalized_address, - raw_email=address, - sponsoring_registrars=self.registrar - ).exists()) - - # Try to add the same person again; should fail - scrambled_email = self.randomize_capitalization(address) - response = self.submit_form('user_management_sponsored_user_add_user', - data={'a-sponsoring_registrars': self.registrar.pk, - 'a-first_name': 'First', - 'a-last_name': 'Last', - 'a-e-address': scrambled_email}, - query_params={'email': scrambled_email}).content - self.assertIn(bytes("{} is already sponsored by your registrar.".format(normalized_address), 'utf-8'), response) - - def test_registrar_user_cannot_add_sponsored_user_to_inaccessible_registrar(self): - self.log_in_user(self.registrar_user) - self.submit_form('user_management_sponsored_user_add_user', - data={'a-sponsoring_registrars': self.unrelated_registrar.pk, - 'a-first_name': 'First', - 'a-last_name': 'Last', - 'a-e-address': 'doesnotexist@example.com'}, - query_params={'email': 'doesnotexist@example.com'}, - error_keys=['sponsoring_registrars']) - self.assertFalse(LinkUser.objects.filter(email='doesnotexist@example.com', - sponsoring_registrars=self.unrelated_registrar).exists()) - - ### ADDING EXISTING USERS TO REGISTRARS AS SPONSORED USERS ### - - def test_admin_user_can_add_sponsorship_to_existing_user(self): - self.log_in_user(self.admin_user) - scrambled_email = self.randomize_capitalization(self.regular_user.email) - self.submit_form('user_management_sponsored_user_add_user', - data={'a-sponsoring_registrars': self.registrar.pk}, - query_params={'email': scrambled_email}, - success_url=reverse('user_management_manage_sponsored_user'), - success_query=LinkUser.objects.filter(pk=self.regular_user.pk, sponsoring_registrars=self.registrar)) - - def test_registrar_user_can_add_sponsorship_to_existing_user(self): - self.log_in_user(self.registrar_user) - scrambled_email = self.randomize_capitalization(self.regular_user.email) - self.submit_form('user_management_sponsored_user_add_user', - data={'a-sponsoring_registrars': self.registrar.pk}, - query_params={'email': scrambled_email}, - success_url=reverse('user_management_manage_sponsored_user'), - success_query=LinkUser.objects.filter(pk=self.regular_user.pk, sponsoring_registrars=self.registrar)) - - def test_registrar_user_cannot_add_sponsorship_for_other_registrar_to_existing_user(self): - self.log_in_user(self.registrar_user) - self.submit_form('user_management_sponsored_user_add_user', - data={'a-sponsoring_registrars': self.unrelated_registrar.pk}, - query_params={'email': self.regular_user.email}, - error_keys=['sponsoring_registrars']) - self.assertFalse(LinkUser.objects.filter(pk=self.regular_user.pk, sponsoring_registrars=self.unrelated_registrar).exists()) - - ### TOGGLING THE STATUS OF SPONSORSHIPS ### - - def test_admin_user_can_deactivate_active_sponsorship(self): - sponsorship = Sponsorship.objects.get(user=self.sponsored_user, registrar=self.registrar, status='active') - self.assertTrue(all(not folder.read_only for folder in sponsorship.folders)) - self.log_in_user(self.admin_user) - self.submit_form('user_management_manage_single_sponsored_user_remove', - reverse_kwargs={'args': [self.sponsored_user.id, self.registrar.id]}, - success_url=reverse('user_management_manage_single_sponsored_user', args=[self.sponsored_user.id])) - sponsorship.refresh_from_db() - self.assertEqual(sponsorship.status, 'inactive') - self.assertTrue(all(folder.read_only for folder in sponsorship.folders)) - - - def test_admin_user_can_reactivate_inactive_sponsorship(self): - sponsorship = Sponsorship.objects.get(user=self.inactive_sponsored_user, registrar=self.registrar, status='inactive') - self.assertTrue(all(folder.read_only for folder in sponsorship.folders)) - self.log_in_user(self.admin_user) - self.submit_form('user_management_manage_single_sponsored_user_readd', - reverse_kwargs={'args': [self.inactive_sponsored_user.id, self.registrar.id]}, - success_url=reverse('user_management_manage_single_sponsored_user', args=[self.inactive_sponsored_user.id])) - sponsorship.refresh_from_db() - self.assertEqual(sponsorship.status, 'active') - self.assertTrue(all(not folder.read_only for folder in sponsorship.folders)) - - def test_registrar_user_can_deactivate_active_sponsorship(self): - sponsorship = Sponsorship.objects.get(user=self.sponsored_user, registrar=self.registrar, status='active') - self.assertTrue(all(not folder.read_only for folder in sponsorship.folders)) - self.log_in_user(self.registrar_user) - self.submit_form('user_management_manage_single_sponsored_user_remove', - reverse_kwargs={'args': [self.sponsored_user.id, self.registrar.id]}, - success_url=reverse('user_management_manage_single_sponsored_user', args=[self.sponsored_user.id])) - sponsorship.refresh_from_db() - self.assertEqual(sponsorship.status, 'inactive') - self.assertTrue(all(folder.read_only for folder in sponsorship.folders)) - - def test_registrar_user_cannot_deactivate_active_sponsorship_for_other_registrar(self): - self.assertTrue(self.unrelated_registrar in self.another_sponsored_user.sponsoring_registrars.all()) - self.log_in_user(self.registrar_user) - self.submit_form('user_management_manage_single_sponsored_user_remove', - reverse_kwargs={'args': [self.another_sponsored_user.id, self.unrelated_registrar.id]}, - require_status_code=404) - - def test_registrar_user_can_reactivate_inactive_sponsorship(self): - sponsorship = Sponsorship.objects.get(user=self.inactive_sponsored_user, registrar=self.registrar, status='inactive') - self.assertTrue(all(folder.read_only for folder in sponsorship.folders)) - self.log_in_user(self.registrar_user) - self.submit_form('user_management_manage_single_sponsored_user_readd', - reverse_kwargs={'args': [self.inactive_sponsored_user.id, self.registrar.id]}, - success_url=reverse('user_management_manage_single_sponsored_user', args=[self.inactive_sponsored_user.id])) - sponsorship.refresh_from_db() - self.assertEqual(sponsorship.status, 'active') - self.assertTrue(all(not folder.read_only for folder in sponsorship.folders)) - - def test_registrar_user_cannot_reactivate_inactive_sponsorship_for_other_registrar(self): - sponsorship = Sponsorship.objects.get(user=self.another_inactive_sponsored_user, registrar=self.unrelated_registrar, status='inactive') - self.log_in_user(self.registrar_user) - self.submit_form('user_management_manage_single_sponsored_user_readd', - reverse_kwargs={'args': [self.another_inactive_sponsored_user.id, self.unrelated_registrar.id]}, - require_status_code=404) - sponsorship.refresh_from_db() - self.assertEqual(sponsorship.status, 'inactive') - ### ADDING NEW USERS TO REGISTRARS AS REGISTRAR USERS) ### def test_admin_user_can_add_new_user_to_registrar(self): From 58f5b8a2a34f349e19979f0cd8f51f333fe313d3 Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Thu, 16 Jan 2025 15:39:18 -0500 Subject: [PATCH 12/20] Fix flakey test (when fixture users have the same last name) --- perma_web/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/perma_web/conftest.py b/perma_web/conftest.py index 36d00c097..d2d8a0415 100644 --- a/perma_web/conftest.py +++ b/perma_web/conftest.py @@ -665,7 +665,7 @@ def sponsored_user_list(sponsored_user_factory): users.append(sponsored_user_factory()) user_data = [] - for user in sorted(users, key=lambda u: u.last_name): + for user in sorted(users, key=lambda u: (u.last_name, u.first_name)): user_data.append((user.email, user.sponsorships.first().status)) return user_data From dec9bf2d41e3c77411d5fca4411405fa64d28146 Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Thu, 16 Jan 2025 15:39:32 -0500 Subject: [PATCH 13/20] Consistent formatting. --- perma_web/perma/tests/test_views_user_management.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/perma_web/perma/tests/test_views_user_management.py b/perma_web/perma/tests/test_views_user_management.py index ab420200f..01357e6cf 100644 --- a/perma_web/perma/tests/test_views_user_management.py +++ b/perma_web/perma/tests/test_views_user_management.py @@ -521,7 +521,7 @@ def test_cannot_add_registrar_user_to_org(client, admin_user, registrar_user): ### -# REMOVING USERS FROM ORGANIZATIONS ### +### REMOVING USERS FROM ORGANIZATIONS ### ### @pytest.mark.parametrize( From c996abcd77a419d49ad55bd7587e2d2e33e4fd59 Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Thu, 16 Jan 2025 15:44:34 -0500 Subject: [PATCH 14/20] Lint. --- perma_web/conftest.py | 51 ++++++++++--------- .../perma/tests/test_views_user_management.py | 2 +- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/perma_web/conftest.py b/perma_web/conftest.py index d2d8a0415..b790d4ba5 100644 --- a/perma_web/conftest.py +++ b/perma_web/conftest.py @@ -197,7 +197,8 @@ def f(page, user): from dateutil.relativedelta import relativedelta from django.utils import timezone -from perma.models import (Registrar, Organization, LinkUser, UserOrganizationAffiliation, +from perma.models import ( + Registrar, Organization, LinkUser, UserOrganizationAffiliation, Link, CaptureJob, Capture, Sponsorship, Folder ) from perma.utils import pp_date_from_post @@ -352,18 +353,18 @@ class PayingRegistrarUserFactory(LinkUserFactory): @register_factory class UnconfirmedRegistrarUserFactory( - UnactivatedUserFactory, - RegistrarUserFactory - ): - pass + UnactivatedUserFactory, + RegistrarUserFactory +): + pass @register_factory class DeactivatedRegistrarUserFactory( - DeactivatedUserFactory, - RegistrarUserFactory - ): - pass + DeactivatedUserFactory, + RegistrarUserFactory +): + pass @register_factory @@ -391,17 +392,17 @@ class SponsoredUserFactory(LinkUserFactory): @register_factory class UnconfirmedSponsoredUserFactory( - UnactivatedUserFactory, - SponsoredUserFactory - ): - pass + UnactivatedUserFactory, + SponsoredUserFactory +): + pass @register_factory class DeactivatedSponsoredUserFactory( - DeactivatedUserFactory, - SponsoredUserFactory - ): - pass + DeactivatedUserFactory, + SponsoredUserFactory +): + pass @register_factory class InactiveSponsoredUserFactory(LinkUserFactory): @@ -451,18 +452,18 @@ class OrgUserFactory(LinkUserFactory): @register_factory class UnconfirmedOrgUserFactory( - UnactivatedUserFactory, - OrgUserFactory - ): - pass + UnactivatedUserFactory, + OrgUserFactory +): + pass @register_factory class DeactivatedOrgUserFactory( - DeactivatedUserFactory, - OrgUserFactory - ): - pass + DeactivatedUserFactory, + OrgUserFactory +): + pass @register_factory diff --git a/perma_web/perma/tests/test_views_user_management.py b/perma_web/perma/tests/test_views_user_management.py index 01357e6cf..e5ee20768 100644 --- a/perma_web/perma/tests/test_views_user_management.py +++ b/perma_web/perma/tests/test_views_user_management.py @@ -16,7 +16,7 @@ from django.test import override_settings from django.test.client import RequestFactory -from perma.models import LinkUser, Organization, Registrar, Sponsorship, UserOrganizationAffiliation +from perma.models import LinkUser, Organization, Registrar, UserOrganizationAffiliation from perma.tests.utils import PermaTestCase from perma.forms import MultipleUsersFormWithOrganization From 64fbae5529ec3d7f747584a30d5eca334c2ab6c6 Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Fri, 17 Jan 2025 09:25:57 -0500 Subject: [PATCH 15/20] Convert tests for managing admins. --- perma_web/conftest.py | 10 +- .../perma/tests/test_views_user_management.py | 111 +++++++++++------- 2 files changed, 71 insertions(+), 50 deletions(-) diff --git a/perma_web/conftest.py b/perma_web/conftest.py index b790d4ba5..a58f88462 100644 --- a/perma_web/conftest.py +++ b/perma_web/conftest.py @@ -325,6 +325,11 @@ class Meta: password = Password(TEST_USER_PASSWORD) +@register_factory +class AdminUserFactory(LinkUserFactory): + is_staff = True + + @register_factory class DeactivatedUserFactory(LinkUserFactory): is_active = False @@ -630,11 +635,6 @@ def user_data(user_data_factory): return user_data_factory() -@pytest.fixture -def admin_user(link_user_factory): - return link_user_factory(is_staff=True) - - @pytest.fixture def multi_registrar_org_user(link_user_factory, organization_factory): first = organization_factory() diff --git a/perma_web/perma/tests/test_views_user_management.py b/perma_web/perma/tests/test_views_user_management.py index e5ee20768..7d4360d83 100644 --- a/perma_web/perma/tests/test_views_user_management.py +++ b/perma_web/perma/tests/test_views_user_management.py @@ -841,6 +841,72 @@ def test_registrar_user_cannot_reactivate_inactive_sponsorship_for_other_registr assert sponsorship.status == 'inactive' +### +### ADDING ADMINS ### +### + +def test_admin_user_can_add_new_user_as_admin(client, admin_user, user_data): + client.force_login(admin_user) + + submit_form( + client, + 'user_management_admin_user_add_user', + data={ + 'a-first_name': user_data['first_name'], + 'a-last_name': user_data['last_name'], + 'a-e-address': user_data['email'] + }, + success_url=reverse('user_management_manage_admin_user'), + success_query=LinkUser.objects.filter( + email=user_data['normalized_email'], + raw_email=user_data['email'], + is_staff=True + ) + ) + + +def test_admin_user_can_add_existing_user_as_admin(client, admin_user, link_user): + client.force_login(admin_user) + + submit_form( + client, + url=f"{reverse('user_management_admin_user_add_user')}?email={randomize_capitalization(link_user.email)}", + success_url=reverse('user_management_manage_admin_user'), + success_query=LinkUser.objects.filter(id=link_user.id, is_staff=True) + ) + + +### DEMOTING ADMINS ### + +def test_can_remove_admin_privileges(client, admin_user_factory): + admin_user = admin_user_factory() + another_admin_user = admin_user_factory() + assert another_admin_user.is_staff + + client.force_login(admin_user) + submit_form( + client, + url=reverse('user_management_manage_single_admin_user_remove', args=[another_admin_user.id]), + success_url=reverse('user_management_manage_admin_user') + ) + + another_admin_user.refresh_from_db() + assert not another_admin_user.is_staff + + +def test_can_remove_own_admin_privileges(client, admin_user): + assert admin_user.is_staff + client.force_login(admin_user) + submit_form( + client, + url=reverse('user_management_manage_single_admin_user_remove', args=[admin_user.id]), + success_url=reverse('create_link') + ) + + admin_user.refresh_from_db() + assert not admin_user.is_staff + + ### ### EXPORT USER LISTS ### @@ -1513,51 +1579,6 @@ def test_can_remove_self_from_registrar(self): success_url=reverse('create_link')) self.assertFalse(LinkUser.objects.filter(pk=self.registrar_user.pk, registrar=self.registrar).exists()) - ### ADDING NEW USERS AS ADMINS ### - - def test_admin_user_can_add_new_user_as_admin(self): - address = self.randomize_capitalization('doesnotexist@example.com') - normalized_address = address.lower() - self.log_in_user(self.admin_user) - self.submit_form('user_management_admin_user_add_user', - data={'a-first_name': 'First', - 'a-last_name': 'Last', - 'a-e-address': address}, - query_params={'email': address}, - success_url=reverse('user_management_manage_admin_user'), - success_query=LinkUser.objects.filter( - email=normalized_address, - raw_email=address, - is_staff=True).exists() - ) - - ### ADDING EXISTING USERS AS ADMINS ### - - def test_admin_user_can_add_existing_user_as_admin(self): - self.log_in_user(self.admin_user) - self.submit_form('user_management_admin_user_add_user', - query_params={'email': self.randomize_capitalization(self.regular_user.email)}, - success_url=reverse('user_management_manage_admin_user'), - success_query=LinkUser.objects.filter(pk=self.regular_user.pk, is_staff=True)) - - ### REMOVING USERS AS ADMINS ### - - def test_can_remove_user_from_admin(self): - self.log_in_user(self.admin_user) - self.regular_user.is_staff = True - self.regular_user.save() - self.submit_form('user_management_manage_single_admin_user_remove', - reverse_kwargs={'args': [self.regular_user.pk]}, - success_url=reverse('user_management_manage_admin_user')) - self.assertFalse(LinkUser.objects.filter(pk=self.regular_user.pk, is_staff=True).exists()) - - def test_can_remove_self_from_admin(self): - self.log_in_user(self.admin_user) - self.submit_form('user_management_manage_single_admin_user_remove', - reverse_kwargs={'args': [self.admin_user.pk]}, - success_url=reverse('create_link')) - self.assertFalse(LinkUser.objects.filter(pk=self.admin_user.pk, is_staff=True).exists()) - ### ### SIGNUP From d341e9db93eeaa527c8cf5f6840c02559802e35f Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Fri, 17 Jan 2025 14:27:50 -0500 Subject: [PATCH 16/20] Convert tests for managing registrar users. --- perma_web/perma/tests/test_permissions.py | 9 +- .../perma/tests/test_views_user_management.py | 496 ++++++++++++------ perma_web/perma/views/user_management.py | 14 +- 3 files changed, 357 insertions(+), 162 deletions(-) diff --git a/perma_web/perma/tests/test_permissions.py b/perma_web/perma/tests/test_permissions.py index 2c3db0e49..1008c6d60 100644 --- a/perma_web/perma/tests/test_permissions.py +++ b/perma_web/perma/tests/test_permissions.py @@ -76,6 +76,7 @@ def test_permissions(client, admin_user, registrar_user, org_user, link_user_fac { 'urls': [ ['user_management_manage_single_registrar', {'kwargs':{'registrar_id': registrar_user_registrar.id}}], + ['user_management_manage_single_registrar_user_remove', {'kwargs':{'user_id': registrar_user.id}}], ], 'allowed': {admin_user, registrar_user}, }, @@ -109,20 +110,12 @@ def test_permissions(client, admin_user, registrar_user, org_user, link_user_fac ], 'allowed': {admin_user, org_user_registrar_user, org_user}, }, - { - 'urls': [ - ['user_management_manage_single_registrar_user_remove', {'kwargs':{'user_id': registrar_user.id}}], - ], - 'allowed': {registrar_user} - }, - { 'urls': [ ['user_management_organization_user_leave_organization', {'kwargs':{'org_id': org_user_org.id}}], ], 'allowed': {org_user} }, - { 'urls': [ ['settings_profile'], diff --git a/perma_web/perma/tests/test_views_user_management.py b/perma_web/perma/tests/test_views_user_management.py index 7d4360d83..ad6942122 100644 --- a/perma_web/perma/tests/test_views_user_management.py +++ b/perma_web/perma/tests/test_views_user_management.py @@ -757,6 +757,7 @@ def test_cannot_create_duplicative_sponsorships(client, admin_user, sponsored_us ) assert b"Select a valid choice. That choice is not one of the available choices" in response.content + ### ### TOGGLING SPONSORSHIP STATUS ### ### @@ -841,6 +842,351 @@ def test_registrar_user_cannot_reactivate_inactive_sponsorship_for_other_registr assert sponsorship.status == 'inactive' +### +### ADDING REGISTRAR USERS ### +### + +@pytest.mark.parametrize( + "user_type", + [ + "registrar_user", + "admin_user" + ] +) +def test_can_add_new_user_to_registrar(user_type, request, client, user_data): + user = request.getfixturevalue(user_type) + client.force_login(user) + + match user_type: + case "registrar_user": + registrar = user.registrar + case "admin_user": + registrar = request.getfixturevalue("registrar") + + submit_form( + client, + url=f"{reverse('user_management_registrar_user_add_user')}?email={user_data['email']}", + data={ + "a-registrar": registrar.id, + "a-first_name": user_data['first_name'], + "a-last_name": user_data['last_name'], + "a-e-address": user_data['email'], + }, + success_url=reverse("user_management_manage_registrar_user"), + success_query=LinkUser.objects.filter( + email=user_data['normalized_email'], + raw_email=user_data['email'], + registrar=registrar + ) + ) + + +def test_cannot_add_new_user_to_inaccessible_registrar(client, registrar_user, user_data, registrar_factory): + client.force_login(registrar_user) + unrelated_registrar = registrar_factory() + + submit_form( + client, + url=f"{reverse('user_management_registrar_user_add_user')}?email={user_data['email']}", + data={ + "a-registrar": unrelated_registrar.id, + "a-first_name": user_data['first_name'], + "a-last_name": user_data['last_name'], + "a-e-address": user_data['email'], + }, + error_keys=['registrar'] + ) + assert not LinkUser.objects.filter(email__iexact=user_data['email']) + + +@pytest.mark.parametrize( + "user_type", + [ + "registrar_user", + "admin_user" + ] +) +def test_can_add_existing_user_to_registrar(user_type, request, client, link_user): + user = request.getfixturevalue(user_type) + client.force_login(user) + + match user_type: + case "registrar_user": + registrar = user.registrar + case "admin_user": + registrar = request.getfixturevalue("registrar") + + scrambled_email = randomize_capitalization(link_user.email) + submit_form( + client, + url=f"{reverse('user_management_registrar_user_add_user')}?email={scrambled_email}", + data={ + "a-registrar": registrar.id + }, + success_url=reverse("user_management_manage_registrar_user"), + ) + + link_user.refresh_from_db() + assert link_user.registrar == registrar + + +def test_cannot_add_existing_user_to_inaccessible_registrar(client, registrar_user, registrar_factory, link_user): + client.force_login(registrar_user) + unrelated_registrar = registrar_factory() + + scrambled_email = randomize_capitalization(link_user.email) + submit_form( + client, + url=f"{reverse('user_management_registrar_user_add_user')}?email={scrambled_email}", + data={ + "a-registrar": unrelated_registrar.id, + }, + error_keys=['registrar'] + ) + + link_user.refresh_from_db() + assert not link_user.registrar + + +@pytest.mark.parametrize( + "user_type", + [ + "registrar_user", + "admin_user" + ] +) +def test_cannot_readd_user_to_registrar(user_type, request, client, registrar_user): + user = request.getfixturevalue(user_type) + client.force_login(user) + + match user_type: + case "registrar_user": + registrar = user.registrar + case "admin_user": + registrar = registrar_user.registrar + + response = submit_form( + client, + url=f"{reverse('user_management_registrar_user_add_user')}?email={registrar_user.email}", + data={ + "a-registrar": registrar.id + } + ) + + assert b"already a registrar user" in response.content + + +def test_registrar_user_cannot_change_registrar_users_registrar(client, registrar_user, registrar_user_factory): + unrelated_registrar_user = registrar_user_factory() + client.force_login(registrar_user) + + response = submit_form( + client, + url=f"{reverse('user_management_registrar_user_add_user')}?email={unrelated_registrar_user.email}", + data={ + "a-registrar": registrar_user.registrar.id + } + ) + + unrelated_registrar_user.refresh_from_db() + assert b"is already a member" in response.content + assert registrar_user.registrar != unrelated_registrar_user.registrar + + +def test_admin_user_can_change_registrar_users_registrar(client, admin_user, registrar_user, registrar_factory): + unrelated_registrar = registrar_factory() + client.force_login(admin_user) + + submit_form( + client, + url=f"{reverse('user_management_registrar_user_add_user')}?email={registrar_user.email}", + data={ + "a-registrar": unrelated_registrar.id + }, + success_url=reverse("user_management_manage_registrar_user"), + ) + + registrar_user.refresh_from_db() + assert registrar_user.registrar == unrelated_registrar + + +@pytest.mark.parametrize( + "user_type", + [ + "registrar_user", + "admin_user" + ] +) +def test_can_upgrade_org_user_to_registrar(user_type, request, client, link_user): + user = request.getfixturevalue(user_type) + client.force_login(user) + + match user_type: + case "registrar_user": + registrar = user.registrar + case "admin_user": + registrar = request.getfixturevalue("registrar") + link_user.organizations.set([registrar.organizations.get()]) + assert link_user.is_organization_user + + submit_form( + client, + url=f"{reverse('user_management_registrar_user_add_user')}?email={link_user.email}", + data={ + "a-registrar": registrar.id + }, + success_url=reverse("user_management_manage_registrar_user") + ) + + link_user.refresh_from_db() + assert link_user.registrar == registrar + assert not link_user.organizations.exists() + + +def test_registrar_user_cannot_upgrade_unrelated_org_user_to_registrar(client, registrar_user, org_user): + client.force_login(registrar_user) + + response = submit_form( + client, + url=f"{reverse('user_management_registrar_user_add_user')}?email={org_user.email}", + data={ + "a-registrar": registrar_user.registrar.id + } + ) + + assert b"belongs to organizations that are not controlled by your registrar" in response.content + org_user.refresh_from_db() + assert not org_user.registrar + assert org_user.organizations.exists() + + +@pytest.mark.parametrize( + "user_type", + [ + "registrar_user", + "admin_user" + ] +) +def test_cannot_upgrade_multi_registrar_org_user_to_registrar(user_type, request, client, link_user, organization_factory): + user = request.getfixturevalue(user_type) + client.force_login(user) + + match user_type: + case "registrar_user": + registrar = user.registrar + case "admin_user": + registrar = request.getfixturevalue("registrar") + + unrelated_org = organization_factory() + link_user.organizations.set([registrar.organizations.get(), unrelated_org]) + assert link_user.organizations.count() == 2 + + response = submit_form( + client, + url=f"{reverse('user_management_registrar_user_add_user')}?email={link_user.email}", + data={ + "a-registrar": registrar.id + } + ) + + assert b"You cannot make them a registrar" in response.content + link_user.refresh_from_db() + assert not link_user.registrar + assert link_user.organizations.count() == 2 + + +@pytest.mark.parametrize( + "user_type", + [ + "registrar_user", + "admin_user" + ] +) +def test_cannot_add_admin_user_to_registrar(user_type, request, client, admin_user_factory): + user = request.getfixturevalue(user_type) + client.force_login(user) + admin_user = admin_user_factory() + + match user_type: + case "registrar_user": + registrar = user.registrar + case "admin_user": + registrar = request.getfixturevalue("registrar") + + response = submit_form( + client, + url=f"{reverse('user_management_registrar_user_add_user')}?email={admin_user.email}", + data={ + "a-registrar": registrar.id + } + ) + + assert b"is an admin user" in response.content + admin_user.refresh_from_db() + assert not admin_user.registrar + assert admin_user.is_staff + + +### +### REMOVING REGISTRAR USERS ### +### + +@pytest.mark.parametrize( + "user_type", + [ + "registrar_user", + "admin_user" + ] +) +def test_can_remove_user_from_registrar(user_type, request, client, link_user): + user = request.getfixturevalue(user_type) + client.force_login(user) + + match user_type: + case "registrar_user": + registrar = user.registrar + case "admin_user": + registrar = request.getfixturevalue("registrar") + link_user.registrar = registrar + link_user.save() + link_user.refresh_from_db() + assert link_user.is_registrar_user() + + response = submit_form( + client, + url=reverse('user_management_manage_single_registrar_user_remove', args=[link_user.id]), + success_url=reverse('user_management_manage_registrar_user') + ) + + link_user.refresh_from_db() + assert not link_user.is_registrar_user() + + +def test_registrar_cannot_remove_unrelated_user_from_registrar(client, registrar_user_factory): + registrar_user = registrar_user_factory() + unrelated_registrar_user = registrar_user_factory() + client.force_login(registrar_user) + + submit_form( + client, + url=reverse('user_management_manage_single_registrar_user_remove', args=[unrelated_registrar_user.id]), + require_status_code=404 + ) + + +def test_can_remove_self_from_registrar(client, registrar_user): + client.force_login(registrar_user) + + response = submit_form( + client, + url=reverse('user_management_manage_single_registrar_user_remove', args=[registrar_user.id]), + success_url=reverse('create_link') + ) + + registrar_user.refresh_from_db() + assert not registrar_user.is_registrar_user() + + ### ### ADDING ADMINS ### ### @@ -1430,156 +1776,6 @@ def initialize_form(csv_file, data=None): self.assertEqual("johndoe@example.com", next(iter(form5.ineligible_users))) - ### ADDING NEW USERS TO REGISTRARS AS REGISTRAR USERS) ### - - def test_admin_user_can_add_new_user_to_registrar(self): - address = self.randomize_capitalization('doesnotexist@example.com') - normalized_address = address.lower() - self.log_in_user(self.admin_user) - self.submit_form('user_management_registrar_user_add_user', - data={'a-registrar': self.registrar.pk, - 'a-first_name': 'First', - 'a-last_name': 'Last', - 'a-e-address': address}, - query_params={'email': address}, - success_url=reverse('user_management_manage_registrar_user'), - success_query=LinkUser.objects.filter( - email=normalized_address, - raw_email=address, - registrar=self.registrar).exists() - ) - - def test_registrar_user_can_add_new_user_to_registrar(self): - address = self.randomize_capitalization('doesnotexist@example.com') - normalized_address = address.lower() - self.log_in_user(self.registrar_user) - self.submit_form('user_management_registrar_user_add_user', - data={'a-registrar': self.registrar.pk, - 'a-first_name': 'First', - 'a-last_name': 'Last', - 'a-e-address': address}, - query_params={'email': address}, - success_url=reverse('user_management_manage_registrar_user'), - success_query=LinkUser.objects.filter( - email=normalized_address, - raw_email=address, - registrar=self.registrar).exists() - ) - - # Try to add the same person again; should fail - scrambled_email = self.randomize_capitalization(address) - response = self.submit_form('user_management_registrar_user_add_user', - data={'a-registrar': self.registrar.pk, - 'a-first_name': 'First', - 'a-last_name': 'Last', - 'a-e-address': scrambled_email}, - query_params={'email': scrambled_email}).content - self.assertIn(bytes("{} is already a registrar user for your registrar.".format(normalized_address), 'utf-8'), response) - - def test_registrar_user_cannot_add_new_user_to_inaccessible_registrar(self): - self.log_in_user(self.registrar_user) - self.submit_form('user_management_registrar_user_add_user', - data={'a-registrar': self.unrelated_registrar.pk, - 'a-first_name': 'First', - 'a-last_name': 'Last', - 'a-e-address': 'doesnotexist@example.com'}, - query_params={'email': 'doesnotexist@example.com'}, - error_keys=['registrar']) - self.assertFalse(LinkUser.objects.filter(email='doesnotexist@example.com', - registrar=self.unrelated_registrar).exists()) - - ### ADDING EXISTING USERS TO REGISTRARS ### - - def add_registrars(self): - # submit email with the same capitalization - self.submit_form('user_management_registrar_user_add_user', - data={'a-registrar': self.registrar.pk}, - query_params={'email': self.regular_user.email}, - success_url=reverse('user_management_manage_registrar_user'), - success_query=LinkUser.objects.filter(pk=self.regular_user.pk, registrar=self.registrar)) - - # submit email with a different capitalization - scrambled_email = self.randomize_capitalization(self.another_regular_user.email) - self.submit_form('user_management_registrar_user_add_user', - data={'a-registrar': self.registrar.pk}, - query_params={'email': scrambled_email}, - success_url=reverse('user_management_manage_registrar_user'), - success_query=LinkUser.objects.filter(pk=self.another_regular_user.pk, registrar=self.registrar)) - - def test_admin_user_can_add_existing_user_to_registrar(self): - self.log_in_user(self.admin_user) - self.add_registrars() - - def test_registrar_user_can_add_existing_user_to_registrar(self): - self.log_in_user(self.registrar_user) - self.add_registrars() - - def test_registrar_user_can_upgrade_org_user_to_registrar(self): - self.log_in_user(self.registrar_user) - self.submit_form('user_management_registrar_user_add_user', - data={'a-registrar': self.registrar.pk}, - query_params={'email': self.organization_user.email}, - success_url=reverse('user_management_manage_registrar_user'), - success_query=LinkUser.objects.filter(pk=self.organization_user.pk, registrar=self.registrar)) - self.assertFalse(LinkUser.objects.filter(pk=self.organization_user.pk, organizations=self.organization).exists()) - - def test_registrar_user_cannot_upgrade_unrelated_org_user_to_registrar(self): - self.log_in_user(self.registrar_user) - resp = self.submit_form('user_management_registrar_user_add_user', - data={'a-registrar': self.registrar.pk}, - query_params={'email': self.unrelated_organization_user.email}) - self.assertIn(b"belongs to organizations that are not controlled by your registrar", resp.content) - self.assertFalse(LinkUser.objects.filter(pk=self.unrelated_organization_user.pk, registrar=self.registrar).exists()) - - def test_registrar_user_cannot_add_existing_user_to_inaccessible_registrar(self): - self.log_in_user(self.registrar_user) - self.submit_form('user_management_registrar_user_add_user', - data={'a-registrar': self.unrelated_registrar.pk}, - query_params={'email': self.regular_user.email}, - error_keys=['registrar']) - self.assertFalse(LinkUser.objects.filter(pk=self.regular_user.pk, registrar=self.unrelated_registrar).exists()) - - def test_cannot_add_admin_user_to_registrar(self): - self.log_in_user(self.registrar_user) - resp = self.submit_form('user_management_registrar_user_add_user', - data={'a-registrar': self.registrar.pk}, - query_params={'email': self.admin_user.email}) - self.assertIn(b"is an admin user", resp.content) - self.assertFalse(LinkUser.objects.filter(pk=self.admin_user.pk, registrar=self.registrar).exists()) - - def test_cannot_add_registrar_user_to_registrar(self): - self.log_in_user(self.registrar_user) - resp = self.submit_form('user_management_registrar_user_add_user', - data={'a-registrar': self.registrar.pk}, - query_params={'email': self.unrelated_registrar_user.email}) - self.assertIn(b"is already a member of another registrar", resp.content) - self.assertFalse(LinkUser.objects.filter(pk=self.unrelated_registrar_user.pk, registrar=self.registrar).exists()) - - ### REMOVING REGISTRAR USERS FROM REGISTRARS ### - - def test_can_remove_user_from_registrar(self): - self.log_in_user(self.registrar_user) - self.regular_user.registrar = self.registrar - self.regular_user.save() - self.submit_form('user_management_manage_single_registrar_user_remove', - reverse_kwargs={'args': [self.regular_user.pk]}, - success_url=reverse('user_management_manage_registrar_user')) - self.assertFalse(LinkUser.objects.filter(pk=self.regular_user.pk, registrar=self.registrar).exists()) - - def test_registrar_cannot_remove_unrelated_user_from_registrar(self): - self.log_in_user(self.registrar_user) - self.submit_form('user_management_manage_single_registrar_user_remove', - reverse_kwargs={'args': [self.unrelated_registrar_user.pk]}, - require_status_code=404) - - def test_can_remove_self_from_registrar(self): - self.log_in_user(self.registrar_user) - self.submit_form('user_management_manage_single_registrar_user_remove', - reverse_kwargs={'args': [self.registrar_user.pk]}, - success_url=reverse('create_link')) - self.assertFalse(LinkUser.objects.filter(pk=self.registrar_user.pk, registrar=self.registrar).exists()) - - ### ### SIGNUP ### diff --git a/perma_web/perma/views/user_management.py b/perma_web/perma/views/user_management.py index 29a397520..a145d647a 100755 --- a/perma_web/perma/views/user_management.py +++ b/perma_web/perma/views/user_management.py @@ -912,6 +912,14 @@ def target_user_valid(self): return False, f"{self.object} is already a member of another registrar and cannot be added to your registrar." if self.object.organizations.exclude(registrar=self.request.user.registrar).exists(): return False, f"{self.object} belongs to organizations that are not controlled by your registrar. You cannot make them a registrar unless they leave those organizations." + + if self.object.registrar_id: + if not 'registrar' in self.get_form().changed_data: + return False, f"{self.object} is already a registrar user for that registrar." + + if len(set(org.registrar_id for org in self.object.organizations.all())) > 1: + return False, f"{self.object} is associated with the organizations of multiple registrars. You cannot make them a registrar unless they leave one registrars' organizations." + return True, "" @@ -1037,16 +1045,14 @@ def manage_single_organization_user_remove(request, user_id): return HttpResponseRedirect(reverse('user_management_manage_organization_user')) -@user_passes_test_or_403(lambda user: user.is_registrar_user()) +@user_passes_test_or_403(lambda user: user.is_registrar_user() or user.is_staff) def manage_single_registrar_user_remove(request, user_id): """ Remove a registrar user from a registrar. """ target_user = get_object_or_404(LinkUser, id=user_id) - - # Registrar users can only edit their own registrar users - if request.user.registrar_id != target_user.registrar_id: + if not request.user.shares_scope_with_user(target_user): return HttpResponseForbidden() context = {'target_user': target_user, From 45f914df2fbbca7ccdab7dc34969a310fbea3cce Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Fri, 17 Jan 2025 14:29:10 -0500 Subject: [PATCH 17/20] Lint. --- perma_web/perma/tests/test_views_user_management.py | 4 ++-- perma_web/perma/views/user_management.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/perma_web/perma/tests/test_views_user_management.py b/perma_web/perma/tests/test_views_user_management.py index ad6942122..f178962e5 100644 --- a/perma_web/perma/tests/test_views_user_management.py +++ b/perma_web/perma/tests/test_views_user_management.py @@ -1152,7 +1152,7 @@ def test_can_remove_user_from_registrar(user_type, request, client, link_user): link_user.refresh_from_db() assert link_user.is_registrar_user() - response = submit_form( + submit_form( client, url=reverse('user_management_manage_single_registrar_user_remove', args=[link_user.id]), success_url=reverse('user_management_manage_registrar_user') @@ -1177,7 +1177,7 @@ def test_registrar_cannot_remove_unrelated_user_from_registrar(client, registrar def test_can_remove_self_from_registrar(client, registrar_user): client.force_login(registrar_user) - response = submit_form( + submit_form( client, url=reverse('user_management_manage_single_registrar_user_remove', args=[registrar_user.id]), success_url=reverse('create_link') diff --git a/perma_web/perma/views/user_management.py b/perma_web/perma/views/user_management.py index a145d647a..9822001e7 100755 --- a/perma_web/perma/views/user_management.py +++ b/perma_web/perma/views/user_management.py @@ -914,7 +914,7 @@ def target_user_valid(self): return False, f"{self.object} belongs to organizations that are not controlled by your registrar. You cannot make them a registrar unless they leave those organizations." if self.object.registrar_id: - if not 'registrar' in self.get_form().changed_data: + if 'registrar' not in self.get_form().changed_data: return False, f"{self.object} is already a registrar user for that registrar." if len(set(org.registrar_id for org in self.object.organizations.all())) > 1: From 3a36686e6042648252f20498b0fd9db944c38d00 Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Thu, 23 Jan 2025 12:48:24 -0500 Subject: [PATCH 18/20] Convert and supplement tests for adding/editing multiple org users. --- perma_web/conftest.py | 160 ++++- perma_web/perma/forms.py | 12 +- .../perma/tests/test_views_user_management.py | 666 +++++++++++++++--- perma_web/perma/views/user_management.py | 6 + 4 files changed, 759 insertions(+), 85 deletions(-) diff --git a/perma_web/conftest.py b/perma_web/conftest.py index a58f88462..9bf555ccd 100644 --- a/perma_web/conftest.py +++ b/perma_web/conftest.py @@ -2,7 +2,7 @@ import boto3 from dataclasses import dataclass import os -from random import choice +from random import choice, randrange import subprocess from django.conf import settings @@ -195,8 +195,10 @@ def f(page, user): from decimal import Decimal from datetime import datetime, timezone as tz from dateutil.relativedelta import relativedelta +from django.core.files.uploadedfile import SimpleUploadedFile from django.utils import timezone + from perma.models import ( Registrar, Organization, LinkUser, UserOrganizationAffiliation, Link, CaptureJob, Capture, Sponsorship, Folder @@ -1060,8 +1062,161 @@ def f(customer): return f +# For adding org users via a CSV + +@pytest.fixture +def tsv(): + return SimpleUploadedFile( + 'users.tsv', + FAKE.tsv( + data_columns=('{{first_name}}', '{{last_name}}', '{{email}}'), + num_rows=10, + include_row_ids=False + ).encode('utf-8'), + content_type="text/tsv" + ) + + +@pytest.fixture +def corrupted_csv(): + return SimpleUploadedFile( + 'users.csv', + FAKE.csv( + data_columns=('{{first_name}}', '{{last_name}}', '{{email}}'), + num_rows=10, + include_row_ids=False + ).encode('utf-16'), + content_type="text/csv" + ) + + +def wrap_csv_data_in_file(filename, data): + return SimpleUploadedFile(filename, data.encode('utf-8'), content_type='text/csv') + + +def get_org_user_csv(user_data_factory=None, users=None, skip_headers=None, skip_fields=None, invalid_email=False): + if user_data_factory and users: + raise Exception("Please pass user list or user_data_factory, not both.") + + rows = [] + if not skip_headers: + skip_headers = [] + if not skip_fields: + skip_fields = [] + + # Add headers + all_headers = ["email", "first_name", "last_name"] + headers = [] + match skip_headers: + case 'all': + pass + case _: + for header in all_headers: + if header not in skip_headers: + headers.append(header) + if headers: + rows.append(",".join(headers)) + + # Add fields + all_fields = ["email", "first_name", "last_name"] + + row_count = len(users) if users else 10 + random_row = randrange(0, row_count) + for n in range(row_count): + fields = [] + + if users: + user_data = { + "first_name": users[n].first_name, + "last_name": users[n].last_name, + "email": users[n].raw_email, + } + else: + user_data = user_data_factory() + + match skip_fields: + case 'all': + pass + case _: + for field in all_fields: + if field in skip_fields and n == random_row: + fields.append("") + elif invalid_email and field == 'email' and n == random_row: + fields.append('1@1com') + else: + fields.append(user_data[field]) + + rows.append(",".join(fields)) + + # Add line breaks + csv_data = "\r\n".join(rows) + + # Make it look like a file uploaded via an HTML form, to Django + return wrap_csv_data_in_file('users.csv', csv_data) + + +@pytest.fixture +def org_user_csv_complete(user_data_factory): + return get_org_user_csv(user_data_factory) + + +@pytest.fixture +def org_user_csv_missing_headers(user_data_factory): + def f(skip_headers='all'): + return get_org_user_csv(user_data_factory, skip_headers=skip_headers) + return f + + +@pytest.fixture +def org_user_csv_missing_data(user_data_factory): + def f(skip_fields='all'): + return get_org_user_csv(user_data_factory, skip_fields=skip_fields) + return f + + +@pytest.fixture +def org_user_csv_invalid_email(user_data_factory): + return get_org_user_csv(user_data_factory, invalid_email=True) + + +@pytest.fixture +def org_user_csv_existing_regular_users(link_user_factory): + users = [] + for _ in range(10): + user = link_user_factory() + users.append(user) + return get_org_user_csv(users=users) + + +@pytest.fixture +def org_user_csv_existing_org_users(link_user_factory): + def f(organization): + users = [] + for _ in range(10): + user = link_user_factory() + user.organizations.add(organization) + users.append(user) + return get_org_user_csv(users=users) + return f + + +@pytest.fixture +def org_user_csv_admin_and_registrar(admin_user_factory, registrar_user_factory): + return get_org_user_csv(users=[ + admin_user_factory(), + registrar_user_factory(), + ]) + + # For working with registrars +@pytest.fixture +def registrar_with_five_orgs(registrar_user, organization_factory): + for _ in range(5): + organization_factory(registrar=registrar_user.registrar) + return registrar_user.registrar + + # For working with organizations @pytest.fixture @@ -1297,4 +1452,7 @@ def form_errors(): keys = set(form_errors().keys()) assert set(error_keys) == keys, "Error keys don't match expectations. Expected: %s. Found: %s" % (set(error_keys), keys) + if error_keys is None: + assert not set(form_errors().keys()) + return resp diff --git a/perma_web/perma/forms.py b/perma_web/perma/forms.py index 70403ee28..f6628fcf8 100755 --- a/perma_web/perma/forms.py +++ b/perma_web/perma/forms.py @@ -489,15 +489,17 @@ class Meta: def clean_csv_file(self): file = self.cleaned_data['csv_file'] - # check if file is CSV + # check if file is valid CSV if not file.name.endswith('.csv'): raise forms.ValidationError("The file must be a CSV.") - - file = TextIOWrapper(file, encoding='utf-8') - reader = csv.DictReader(file) + try: + file = TextIOWrapper(file, encoding='utf-8') + reader = csv.DictReader(file) + headers = reader.fieldnames + except Exception: + raise forms.ValidationError("We cannot parse the uploaded file.") # validate the headers - headers = reader.fieldnames if not all(item in headers for item in ['first_name', 'last_name', 'email']): raise forms.ValidationError("CSV file must contain a header row with first_name, last_name and email columns.") diff --git a/perma_web/perma/tests/test_views_user_management.py b/perma_web/perma/tests/test_views_user_management.py index f178962e5..fe6995bc6 100644 --- a/perma_web/perma/tests/test_views_user_management.py +++ b/perma_web/perma/tests/test_views_user_management.py @@ -10,17 +10,14 @@ from bs4 import BeautifulSoup from django.urls import reverse from django.core import mail -from django.core.files.uploadedfile import SimpleUploadedFile from django.conf import settings from django.db import IntegrityError from django.test import override_settings -from django.test.client import RequestFactory from perma.models import LinkUser, Organization, Registrar, UserOrganizationAffiliation from perma.tests.utils import PermaTestCase -from perma.forms import MultipleUsersFormWithOrganization -from conftest import submit_form, randomize_capitalization +from conftest import submit_form, randomize_capitalization, GENESIS ### @@ -520,6 +517,592 @@ def test_cannot_add_registrar_user_to_org(client, admin_user, registrar_user): assert not registrar_user.organizations.exists() +### +### ADDING MULTIPLE ORG USERS VIA CSV +### + + +def test_multiple_org_user_form_populates_org_user_organization( + client, + org_user, + organization_factory +): + unrelated_org = organization_factory() + assert not org_user.organizations.filter(id=unrelated_org.id).exists() + client.force_login(org_user) + + response = client.get( + reverse("user_management_organization_user_add_multiple_users"), + secure=True + ) + + form = response.context["form"] + assert list(form.fields['organizations'].queryset) == list(org_user.organizations.all()) + + +def test_multiple_org_user_form_populates_registrar_user_organizations( + client, + registrar_with_five_orgs, + organization_factory +): + unrelated_org = organization_factory() + assert not registrar_with_five_orgs.organizations.filter(id=unrelated_org.id).exists() + user = registrar_with_five_orgs.users.get() + client.force_login(user) + + response = client.get( + reverse("user_management_organization_user_add_multiple_users"), + secure=True + ) + + form = response.context["form"] + assert list( + form.fields['organizations'].queryset.order_by('name') + ) == list( + registrar_with_five_orgs.organizations.all().order_by('name') + ) + + +def test_multiple_org_user_form_populates_admin_user_organizations( + client, + admin_user, + organization_factory +): + client.force_login(admin_user) + for _ in range(5): + organization_factory() + + response = client.get( + reverse("user_management_organization_user_add_multiple_users"), + secure=True + ) + + form = response.context["form"] + assert list( + form.fields['organizations'].queryset.order_by('name') + ) == list( + Organization.objects.all().order_by('name') + ) + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + "admin_user" + ] +) +def test_multiple_org_user_form_invalid_if_wrong_extension( + user_type, + request, + client, + tsv +): + user = request.getfixturevalue(user_type) + client.force_login(user) + + match user_type: + case "org_user": + org = user.organizations.first() + case "registrar_user": + org = user.registrar.organizations.first() + case "admin_user": + org = request.getfixturevalue("organization") + + response = submit_form( + client, + url = reverse("user_management_organization_user_add_multiple_users"), + data = { + "a-organizations": org.id, + "a-indefinite_affiliation": True, + "a-csv_file": tsv + }, + error_keys=['csv_file'] + ) + assert b"The file must be a CSV" in response.content + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + "admin_user" + ] +) +def test_multiple_org_user_form_invalid_if_unreadable_file( + user_type, + request, + client, + corrupted_csv +): + user = request.getfixturevalue(user_type) + client.force_login(user) + + match user_type: + case "org_user": + org = user.organizations.first() + case "registrar_user": + org = user.registrar.organizations.first() + case "admin_user": + org = request.getfixturevalue("organization") + + response = submit_form( + client, + url = reverse("user_management_organization_user_add_multiple_users"), + data = { + "a-organizations": org.id, + "a-indefinite_affiliation": True, + "a-csv_file": corrupted_csv + }, + error_keys=['csv_file'] + ) + assert b"We cannot parse the uploaded file" in response.content + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + "admin_user" + ] +) +@pytest.mark.parametrize( + "skip_headers", + [ + "all", + "first_name", + "last_name", + "email" + ] +) +def test_multiple_org_user_form_invalid_if_headers_absent( + user_type, + skip_headers, + request, + client, + org_user_csv_missing_headers +): + user = request.getfixturevalue(user_type) + client.force_login(user) + csv = org_user_csv_missing_headers(skip_headers=skip_headers) + + match user_type: + case "org_user": + org = user.organizations.first() + case "registrar_user": + org = user.registrar.organizations.first() + case "admin_user": + org = request.getfixturevalue("organization") + + response = submit_form( + client, + url = reverse("user_management_organization_user_add_multiple_users"), + data = { + "a-organizations": org.id, + "a-indefinite_affiliation": True, + "a-csv_file": csv + }, + error_keys=['csv_file'] + ) + assert b"CSV file must contain a header row with first_name, last_name and email columns." in response.content + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + "admin_user" + ] +) +def test_multiple_org_user_form_invalid_if_no_rows( + user_type, + request, + client, + org_user_csv_missing_data +): + user = request.getfixturevalue(user_type) + client.force_login(user) + csv = org_user_csv_missing_data(skip_fields='all') + + match user_type: + case "org_user": + org = user.organizations.first() + case "registrar_user": + org = user.registrar.organizations.first() + case "admin_user": + org = request.getfixturevalue("organization") + + response = submit_form( + client, + url = reverse("user_management_organization_user_add_multiple_users"), + data = { + "a-organizations": org.id, + "a-indefinite_affiliation": True, + "a-csv_file": csv + }, + error_keys=['csv_file'] + ) + assert b"CSV file must contain at least one user" in response.content + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + "admin_user" + ] +) +def test_multiple_org_user_form_invalid_if_any_email_absent( + user_type, + request, + client, + org_user_csv_missing_data +): + user = request.getfixturevalue(user_type) + client.force_login(user) + csv = org_user_csv_missing_data(skip_fields='email') + + match user_type: + case "org_user": + org = user.organizations.first() + case "registrar_user": + org = user.registrar.organizations.first() + case "admin_user": + org = request.getfixturevalue("organization") + + response = submit_form( + client, + url = reverse("user_management_organization_user_add_multiple_users"), + data = { + "a-organizations": org.id, + "a-indefinite_affiliation": True, + "a-csv_file": csv + }, + error_keys=["csv_file"] + ) + assert b"Each row in the CSV file must contain email." in response.content + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + "admin_user" + ] +) +def test_multiple_org_user_form_invalid_if_any_email_invalid( + user_type, + request, + client, + org_user_csv_invalid_email +): + user = request.getfixturevalue(user_type) + client.force_login(user) + csv = org_user_csv_invalid_email + + match user_type: + case "org_user": + org = user.organizations.first() + case "registrar_user": + org = user.registrar.organizations.first() + case "admin_user": + org = request.getfixturevalue("organization") + + response = submit_form( + client, + url = reverse("user_management_organization_user_add_multiple_users"), + data = { + "a-organizations": org.id, + "a-indefinite_affiliation": True, + "a-csv_file": csv + }, + error_keys=['csv_file'] + ) + assert b"CSV file contains invalid email address" in response.content + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user" + ] +) +def test_multiple_org_user_form_disallows_unrelated_organization( + user_type, + request, + client, + org_user_csv_complete, + organization_factory +): + user = request.getfixturevalue(user_type) + client.force_login(user) + csv = org_user_csv_complete + unrelated_org = organization_factory() + + response = submit_form( + client, + url = reverse("user_management_organization_user_add_multiple_users"), + data = { + "a-organizations": unrelated_org.id, + "a-indefinite_affiliation": True, + "a-csv_file": csv + }, + error_keys=['organizations'] + ) + assert b"That choice is not one of the available choices." in response.content + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + "admin_user" + ] +) +def test_multiple_org_user_form_creates_without_names( + user_type, + request, + client, + org_user_csv_missing_data +): + user = request.getfixturevalue(user_type) + client.force_login(user) + csv = org_user_csv_missing_data(skip_fields='first_name,last_name') + + match user_type: + case "org_user": + org = user.organizations.first() + case "registrar_user": + org = user.registrar.organizations.first() + case "admin_user": + org = request.getfixturevalue("organization") + + response = submit_form( + client, + url = reverse("user_management_organization_user_add_multiple_users"), + data = { + "a-organizations": org.id, + "a-indefinite_affiliation": True, + "a-csv_file": csv + }, + error_keys=None + ) + + form = response.context["form"] + created_user_ids = [user.id for user in form.created_users.values()] + assert len(created_user_ids) == 10 + assert UserOrganizationAffiliation.objects.filter( + user_id__in=created_user_ids, + user__first_name='', + user__last_name='' + ).count() == 1 + assert UserOrganizationAffiliation.objects.filter( + user_id__in=created_user_ids + ).count() == 10 + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + "admin_user" + ] +) +def test_multiple_org_user_form_creates_with_names( + user_type, + request, + client, + org_user_csv_complete +): + user = request.getfixturevalue(user_type) + client.force_login(user) + csv = org_user_csv_complete + + match user_type: + case "org_user": + org = user.organizations.first() + case "registrar_user": + org = user.registrar.organizations.first() + case "admin_user": + org = request.getfixturevalue("organization") + + response = submit_form( + client, + url = reverse("user_management_organization_user_add_multiple_users"), + data = { + "a-organizations": org.id, + "a-indefinite_affiliation": True, + "a-csv_file": csv + }, + error_keys=None + ) + + form = response.context["form"] + created_user_ids = [user.id for user in form.created_users.values()] + assert len(created_user_ids) == 10 + assert not UserOrganizationAffiliation.objects.filter( + user_id__in=created_user_ids, + user__first_name='', + user__last_name='' + ).exists() + assert UserOrganizationAffiliation.objects.filter( + user_id__in=created_user_ids + ).count() == 10 + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + "admin_user" + ] +) +def test_multiple_org_user_form_updates_expiry_date( + user_type, + request, + client, + org_user_csv_existing_org_users +): + user = request.getfixturevalue(user_type) + client.force_login(user) + + match user_type: + case "org_user": + org = user.organizations.first() + case "registrar_user": + org = user.registrar.organizations.first() + case "admin_user": + org = request.getfixturevalue("organization") + + csv = org_user_csv_existing_org_users(org) + affiliations = UserOrganizationAffiliation.objects.filter( + user__in=org.users.exclude(id=user.id) + ) + user_count = affiliations.count() + assert all(affiliation.expires_at is None for affiliation in affiliations.all()) + + response = submit_form( + client, + url = reverse("user_management_organization_user_add_multiple_users"), + data = { + "a-organizations": org.id, + "a-indefinite_affiliation": True, + "a-csv_file": csv, + "a-expires_at": GENESIS + }, + error_keys=None + ) + + form = response.context["form"] + updated_user_ids = [user.id for user in form.updated_users.values()] + assert len(updated_user_ids) == user_count + assert all(affiliation.expires_at == GENESIS for affiliation in affiliations.all()) + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + "admin_user" + ] +) +def test_multiple_org_user_form_upgrades_regular_users( + user_type, + request, + client, + org_user_csv_existing_regular_users +): + user = request.getfixturevalue(user_type) + client.force_login(user) + csv = org_user_csv_existing_regular_users + + match user_type: + case "org_user": + org = user.organizations.first() + case "registrar_user": + org = user.registrar.organizations.first() + case "admin_user": + org = request.getfixturevalue("organization") + + assert not UserOrganizationAffiliation.objects.filter( + user__in=org.users.exclude(id=user.id) + ).exists() + + response = submit_form( + client, + url = reverse("user_management_organization_user_add_multiple_users"), + data = { + "a-organizations": org.id, + "a-indefinite_affiliation": True, + "a-csv_file": csv + }, + error_keys=None + ) + + form = response.context["form"] + updated_user_ids = [user.id for user in form.updated_users.values()] + assert len(updated_user_ids) == 10 + assert UserOrganizationAffiliation.objects.filter( + user__in=org.users.exclude(id=user.id) + ).count() == 10 + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + "admin_user" + ] +) +def test_multiple_org_user_form_rejects_admins_and_registrars( + user_type, + request, + client, + org_user_csv_admin_and_registrar +): + user = request.getfixturevalue(user_type) + client.force_login(user) + csv = org_user_csv_admin_and_registrar + + match user_type: + case "org_user": + org = user.organizations.first() + case "registrar_user": + org = user.registrar.organizations.first() + case "admin_user": + org = request.getfixturevalue("organization") + + response = submit_form( + client, + url = reverse("user_management_organization_user_add_multiple_users"), + data = { + "a-organizations": org.id, + "a-indefinite_affiliation": True, + "a-csv_file": csv + }, + error_keys=None + ) + + form = response.context["form"] + assert len(form.updated_users) == 0 + assert len(form.ineligible_users) == 2 + for email in form.ineligible_users: + ineligible_user = LinkUser.objects.get(email=email) + assert not ineligible_user.organizations.exists() + + ### ### REMOVING USERS FROM ORGANIZATIONS ### ### @@ -1701,81 +2284,6 @@ def test_user_list_filters(self): # status filter tested in test_registrar_user_list_filters - def test_add_multiple_org_users_via_csv(self): - def create_csv_file(filename, content): - return SimpleUploadedFile(filename, content.encode('utf-8'), content_type='text/csv') - - def initialize_form(csv_file, data=None): - data = {'organizations': selected_organization.pk, 'indefinite_affiliation': True} - return MultipleUsersFormWithOrganization(request=request, data=data, files={'csv_file': csv_file}) - - # --- initialize data --- - csv_data = 'first_name,last_name,email\nJohn,Doe,johndoe@example.com\nJane,Smith,janesmith@example.com' - another_csv_data = 'first_name,last_name,email\nJohn2,Doe,john2doe@example.com\nJane2,Smith,jane2smith@example.com' - invalid_csv_data = 'name\nJohn Doe' - another_invalid_csv_data = 'first_name,last_name,email\nJohn,Doe,\nJane,Smith,janesmith@example.com' - - valid_csv_file = create_csv_file('users.csv', csv_data) - another_valid_csv_file = create_csv_file('another_valid_users.csv', another_csv_data) - one_more_valid_csv_file = create_csv_file('one_more_valid_users.csv', csv_data) - invalid_csv_file = create_csv_file('invalid_users.csv', invalid_csv_data) - another_invalid_csv_file = create_csv_file('another_invalid_users.csv', another_invalid_csv_data) - - request = RequestFactory().get('/') - request.user = self.registrar_user - selected_organization = self.another_organization - - # --- test form initialization --- - form = MultipleUsersFormWithOrganization(request=request) - # the registrar user has 3 organizations tied to it as verified in the users.json sample data - self.assertEqual(form.fields['organizations'].queryset.count(), 3) - # confirm that the first item in organization selection field matches the first organization of the registrar - self.assertEqual(form.fields['organizations'].queryset.first(), request.user.registrar.organizations - .order_by('name').first()) - - # --- test csv validation --- - # valid csv - form1 = initialize_form(valid_csv_file) - self.assertTrue(form1.is_valid()) - - # invalid csv - missing headers - form2 = initialize_form(invalid_csv_file) - self.assertFalse(form2.is_valid()) - self.assertTrue("CSV file must contain a header row with first_name, last_name and email columns." - in form2.errors['csv_file']) - - # invalid csv - missing email field - form3 = initialize_form(another_invalid_csv_file) - self.assertFalse(form3.is_valid()) - self.assertTrue("Each row in the CSV file must contain email." - in form3.errors['csv_file']) - - # --- test user creation --- - self.assertTrue(form1.is_valid()) - form1.save(commit=True) - created_user_ids = [user.id for user in form1.created_users.values()] - self.assertEqual(len(created_user_ids), 2) - self.assertEqual(UserOrganizationAffiliation.objects.filter(user_id__in=created_user_ids).count(), 2) - - # --- test user update --- - existing_user = LinkUser.objects.create(email="john2doe@example.com", first_name="John2", last_name="Doe") - form4 = initialize_form(another_valid_csv_file) - self.assertTrue(form4.is_valid()) - form4.save(commit=True) - self.assertEqual(len(form4.updated_users), 1) - self.assertTrue(existing_user in form4.updated_users.values()) - self.assertEqual(len(form4.created_users), 1) - self.assertEqual(next(iter(form4.updated_users)), "john2doe@example.com") - - # --- test validation errors --- - LinkUser.objects.filter(raw_email="johndoe@example.com").update(is_staff=True) - form5 = initialize_form(one_more_valid_csv_file) - self.assertTrue(form5.is_valid()) - form5.save(commit=True) - self.assertEqual(len(form5.ineligible_users), 1) - self.assertEqual("johndoe@example.com", next(iter(form5.ineligible_users))) - - ### ### SIGNUP ### diff --git a/perma_web/perma/views/user_management.py b/perma_web/perma/views/user_management.py index 9822001e7..ee3f60389 100755 --- a/perma_web/perma/views/user_management.py +++ b/perma_web/perma/views/user_management.py @@ -835,6 +835,12 @@ def send_emails(users, email_function, email_template, extra_context, *args): else: add_message(messages.SUCCESS, "Success!", success_message) + if settings.TESTING: + # Calling render causes response.context to be available from + # the Django test client, which in turn gives us access to `form` + # in our tests. + render(self.request, self.template_name, context) + return response From a298c49ce2df2b5a10f312a17d6c05de96d1cc68 Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Fri, 24 Jan 2025 12:04:35 -0500 Subject: [PATCH 19/20] Convert tests for resending activation emails. --- .../perma/tests/test_views_user_management.py | 221 +++++++++++++----- 1 file changed, 167 insertions(+), 54 deletions(-) diff --git a/perma_web/perma/tests/test_views_user_management.py b/perma_web/perma/tests/test_views_user_management.py index fe6995bc6..9ae1b3862 100644 --- a/perma_web/perma/tests/test_views_user_management.py +++ b/perma_web/perma/tests/test_views_user_management.py @@ -1958,6 +1958,173 @@ def test_sponsored_user_export_user_list(flush_db, export_format, mime_type, cli assert index + 1 == len(sponsored_user_list) +### +### RESENDING ACTIVATION EMAILS ### +### + +def resend_should_succeed(client, target_user, mailoutbox): + client.get( + reverse( + 'user_management_resend_activation', args=[target_user.id] + ), + secure=True + ) + + assert len(mailoutbox) == 1 + message = mailoutbox[0] + assert message.subject == "A Perma.cc account has been created for you" + assert message.recipients() == [target_user.raw_email] + + +def resend_should_fail(client, target_user, mailoutbox): + response = client.get( + reverse( + 'user_management_resend_activation', args=[target_user.id] + ), + secure=True + ) + assert response.status_code == 403 + assert len(mailoutbox) == 0 + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user", + "admin_user" + ] +) +def test_can_resend_activation_email_to_org_user( + user_type, + request, + client, + unactivated_user, + mailoutbox +): + user = request.getfixturevalue(user_type) + client.force_login(user) + + match user_type: + case "org_user": + org = user.organizations.first() + case "registrar_user": + org = user.registrar.organizations.first() + case "admin_user": + org = request.getfixturevalue("organization") + target_user = unactivated_user + target_user.organizations.set([org]) + + resend_should_succeed(client, target_user, mailoutbox) + + +@pytest.mark.parametrize( + "user_type", + [ + "org_user", + "registrar_user" + ] +) +def test_cannot_resend_activation_email_to_unrelated_org_user( + user_type, + request, + client, + unconfirmed_org_user_factory, + mailoutbox +): + user = request.getfixturevalue(user_type) + client.force_login(user) + target_user = unconfirmed_org_user_factory() + + resend_should_fail(client, target_user, mailoutbox) + + +@pytest.mark.parametrize( + "user_type", + [ + "registrar_user", + "admin_user" + ] +) +def test_can_resend_activation_email_to_registrar_user( + user_type, + request, + client, + unactivated_user, + mailoutbox +): + user = request.getfixturevalue(user_type) + client.force_login(user) + + match user_type: + case "registrar_user": + registrar = user.registrar + case "admin_user": + registrar = request.getfixturevalue("registrar") + target_user = unactivated_user + target_user.registrar = registrar + target_user.save() + + resend_should_succeed(client, target_user, mailoutbox) + + +def test_cannot_resend_activation_email_to_unrelated_registrar_user( + client, + registrar_user, + unconfirmed_registrar_user_factory, + mailoutbox +): + client.force_login(registrar_user) + target_user = unconfirmed_registrar_user_factory() + + resend_should_fail(client, target_user, mailoutbox) + + +def test_org_user_cannot_resend_activation_email_to_registrar_user( + client, + org_user, + unconfirmed_registrar_user_factory, + mailoutbox +): + client.force_login(org_user) + target_user = unconfirmed_registrar_user_factory( + registrar=org_user.organizations.first().registrar + ) + resend_should_fail(client, target_user, mailoutbox) + + +@pytest.mark.parametrize( + "user_type", + [ + "registrar_user", + "org_user" + ] +) +def test_cannot_resend_activation_email_to_regular_user( + user_type, + request, + client, + unactivated_user, + mailoutbox +): + user = request.getfixturevalue(user_type) + client.force_login(user) + target_user = unactivated_user + + resend_should_fail(client, target_user, mailoutbox) + + +def test_can_resend_activation_email_to_regular_user( + client, + admin_user, + unactivated_user, + mailoutbox +): + client.force_login(admin_user) + target_user = unactivated_user + resend_should_succeed(client, target_user, mailoutbox) + + class UserManagementViewsTestCase(PermaTestCase): @classmethod @@ -2956,60 +3123,6 @@ def test_get_new_activation_code(self): self.assertEqual(len(mail.outbox), 1) self.check_new_activation_email(mail.outbox[0], 'unactivated_faculty_user@example.com') - ### RESENDING ACTIVATION EMAILS ### - - def check_activation_resent(self, user, other_user): - self.get('user_management_resend_activation', - reverse_kwargs={'args':[LinkUser.objects.get(email=other_user).id]}, - user = user) - self.assertEqual(len(mail.outbox), 1) - self.check_new_activation_email(mail.outbox[0], other_user) - - def check_activation_not_resent(self, user, other_user): - self.get('user_management_resend_activation', - reverse_kwargs={'args':[LinkUser.objects.get(email=other_user).id]}, - user = user, - require_status_code = 403) - self.assertEqual(len(mail.outbox), 0) - - # Registrar Users - def test_registrar_can_resend_activation_to_org_user(self): - self.check_activation_resent('test_registrar_user@example.com','test_org_user@example.com') - - def test_registrar_can_resend_activation_to_registrar_user(self): - self.check_activation_resent('another_library_user@example.com','unactivated_registrar_user@example.com') - - def test_registrar_cannot_resend_activation_to_unrelated_org_user(self): - self.check_activation_not_resent('test_registrar_user@example.com','test_yet_another_library_org_user@example.com') - - def test_registrar_cannot_resend_activation_to_regular_user(self): - self.check_activation_not_resent('test_registrar_user@example.com','test_user@example.com') - - def test_registrar_cannot_resend_activation_to_unrelated_registrar_user(self): - self.check_activation_not_resent('test_registrar_user@example.com','another_library_user@example.com') - - # Org Users - def test_org_user_can_resend_activation_to_org_user(self): - self.check_activation_resent('test_org_user@example.com','multi_registrar_org_user@example.com') - - def test_org_user_cannot_resend_activation_to_unrelated_org_user(self): - self.check_activation_not_resent('test_org_user@example.com','test_yet_another_library_org_user@example.com') - - def test_org_user_cannot_resend_activation_to_regular_user(self): - self.check_activation_not_resent('test_org_user@example.com','test_user@example.com') - - def test_org_user_cannot_resend_activation_to_registrar_user(self): - self.check_activation_not_resent('test_org_user@example.com','test_registrar_user@example.com') - - # Admin Users - def test_admin_can_resend_activation_to_regular_user(self): - self.check_activation_resent('test_admin_user@example.com','test_user@example.com') - - def test_admin_can_resend_activation_to_org_user(self): - self.check_activation_resent('test_admin_user@example.com','test_org_user@example.com') - - def test_admin_can_resend_activation_to_registrar_user(self): - self.check_activation_resent('test_admin_user@example.com','test_registrar_user@example.com') ### PASSWORD RESETS ### From de7a0be9d35bfce52e3dd76a05cf50a53819718d Mon Sep 17 00:00:00 2001 From: Rebecca Cremona Date: Fri, 24 Jan 2025 12:36:04 -0500 Subject: [PATCH 20/20] Move signup tests to standalone file. --- perma_web/perma/tests/test_views_signup.py | 707 +++++++++++++++++ .../perma/tests/test_views_user_management.py | 722 +----------------- 2 files changed, 709 insertions(+), 720 deletions(-) create mode 100644 perma_web/perma/tests/test_views_signup.py diff --git a/perma_web/perma/tests/test_views_signup.py b/perma_web/perma/tests/test_views_signup.py new file mode 100644 index 000000000..030b4edf8 --- /dev/null +++ b/perma_web/perma/tests/test_views_signup.py @@ -0,0 +1,707 @@ +from bs4 import BeautifulSoup +from random import random, getrandbits + +from django.core import mail +from django.conf import settings +from django.db import IntegrityError +from django.test import override_settings +from django.urls import reverse + +from perma.models import LinkUser, Registrar +from perma.tests.utils import PermaTestCase + +class UserManagementViewsTestCase(PermaTestCase): + + @classmethod + def setUpTestData(cls): + cls.registrar_user = LinkUser.objects.get(pk=2) + cls.registrar = cls.registrar_user.registrar + + ### Libraries ### + + def new_lib(self): + rand = random() + return { 'email': 'library{}@university.org'.format(rand), + 'name': 'University Library {}'.format(rand), + 'website': 'http://website{}.org'.format(rand), + 'address': '{} Main St., Boston MA 02144'.format(rand)} + + def new_lib_user(self): + rand = random() + email = self.randomize_capitalization('user{}@university.org'.format(rand)) + return { 'raw_email': email, + 'normalized_email': email.lower(), + 'first': 'Joe', + 'last': 'Yacobówski' } + + def check_library_labels(self, soup): + name_label = soup.find('label', {'for': 'id_b-name'}) + self.assertEqual(name_label.text, "Library name") + email_label = soup.find('label', {'for': 'id_b-email'}) + self.assertEqual(email_label.text, "Library email") + website_label = soup.find('label', {'for': 'id_b-website'}) + self.assertEqual(website_label.text, "Library website") + + def check_lib_user_labels(self, soup): + email_label = soup.find('label', {'for': 'id_a-e-address'}) + self.assertEqual(email_label.text, "Your email") + + def check_lib_email(self, message, new_lib, user): + our_address = settings.DEFAULT_FROM_EMAIL + + self.assertIn(new_lib['name'], message.body) + self.assertIn(new_lib['email'], message.body) + + self.assertIn(user['raw_email'], message.body) + + id = Registrar.objects.get(email=new_lib['email']).id + approve_url = "http://testserver{}".format(reverse('user_sign_up_approve_pending_registrar', args=[id])) + self.assertIn(approve_url, message.body) + self.assertEqual(message.subject, "Perma.cc new library registrar account request") + self.assertEqual(message.from_email, our_address) + self.assertEqual(message.recipients(), [our_address]) + self.assertDictEqual(message.extra_headers, {'Reply-To': user['raw_email']}) + + @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) + def test_new_library_render(self): + ''' + Does the library signup form display as expected? + ''' + + # NOT LOGGED IN + + # Registrar and user forms are displayed, + # inputs are blank, and labels are customized as expected + response = self.get('sign_up_libraries').content + soup = BeautifulSoup(response, 'html.parser') + self.check_library_labels(soup) + self.check_lib_user_labels(soup) + inputs = soup.select('input') + self.assertEqual(len(inputs), 9) + for input in inputs: + if input['name'] in ['csrfmiddlewaretoken', 'telephone']: + self.assertTrue(input.get('value', '')) + else: + self.assertFalse(input.get('value', '')) + + # If request_data is present in session, registrar form is prepopulated, + # and labels are still customized as expected + session = self.client.session + new_lib = self.new_lib() + new_lib_user = self.new_lib_user() + session['request_data'] = { 'b-email': new_lib['email'], + 'b-website': new_lib['website'], + 'b-name': new_lib['name'], + 'b-address': new_lib['address'], + 'a-e-address': new_lib_user['raw_email'], + 'a-first_name': new_lib_user['first'], + 'a-last_name': new_lib_user['last'], + 'csrfmiddlewaretoken': '11YY3S2DgOw2DHoWVEbBArnBMdEA2svu' } + session.save() + response = self.get('sign_up_libraries').content + soup = BeautifulSoup(response, 'html.parser') + self.check_library_labels(soup) + self.check_lib_user_labels(soup) + inputs = soup.select('input') + self.assertEqual(len(inputs), 9) + for input in inputs: + if input['name'] in ['csrfmiddlewaretoken', 'telephone']: + self.assertTrue(input.get('value', '')) + elif input['name'][:2] == "b-": + self.assertTrue(input.get('value', '')) + else: + self.assertFalse(input.get('value', '')) + + # If there's an unsuccessful submission, field labels are still as expected. + response = self.post('sign_up_libraries').content + soup = BeautifulSoup(response, 'html.parser') + self.check_library_labels(soup) + self.check_lib_user_labels(soup) + + # LOGGED IN + + # Registrar form is displayed, but user form is not, + # inputs are blank, and labels are still customized as expected + response = self.get('sign_up_libraries', user="test_user@example.com").content + soup = BeautifulSoup(response, 'html.parser') + self.check_library_labels(soup) + inputs = soup.select('input') + self.assertEqual(len(inputs), 6) # 6 because csrf is here and in the logout form + for input in inputs: + self.assertIn(input['name'],['csrfmiddlewaretoken', 'b-name', 'b-email', 'b-website', 'b-address']) + if input['name'] == 'csrfmiddlewaretoken': + self.assertTrue(input.get('value', '')) + else: + self.assertFalse(input.get('value', '')) + + @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) + def test_new_library_submit_success(self): + ''' + Does the library signup form submit as expected? Success cases. + ''' + expected_emails_sent = 0 + + # Not logged in, submit all fields sans first and last name + new_lib = self.new_lib() + new_lib_user = self.new_lib_user() + self.submit_form('sign_up_libraries', + data = { 'b-email': new_lib['email'], + 'b-website': new_lib['website'], + 'b-name': new_lib['name'], + 'a-e-address': new_lib_user['raw_email'] }, + success_url=reverse('register_library_instructions')) + expected_emails_sent += 2 + self.assertEqual(len(mail.outbox), expected_emails_sent) + self.check_lib_email(mail.outbox[expected_emails_sent - 2], new_lib, new_lib_user) + self.check_new_activation_email(mail.outbox[expected_emails_sent - 1], new_lib_user['raw_email']) + + # Not logged in, submit all fields including first and last name + new_lib = self.new_lib() + new_lib_user = self.new_lib_user() + self.submit_form('sign_up_libraries', + data = { 'b-email': new_lib['email'], + 'b-website': new_lib['website'], + 'b-name': new_lib['name'], + 'a-e-address': new_lib_user['raw_email'], + 'a-first_name': new_lib_user['first'], + 'a-last_name': new_lib_user['last']}, + success_url=reverse('register_library_instructions')) + expected_emails_sent += 2 + self.assertEqual(len(mail.outbox), expected_emails_sent) + self.check_lib_email(mail.outbox[expected_emails_sent - 2], new_lib, new_lib_user) + self.check_new_activation_email(mail.outbox[expected_emails_sent - 1], new_lib_user['raw_email']) + + # Logged in + new_lib = self.new_lib() + existing_lib_user = { + 'raw_email': 'test_user@example.com', + 'normalized_email': 'test_user@example.com', + } + self.submit_form('sign_up_libraries', + data = { 'b-email': new_lib['email'], + 'b-website': new_lib['website'], + 'b-name': new_lib['name'] }, + success_url=reverse('settings_affiliations'), + user=existing_lib_user['raw_email']) + expected_emails_sent += 1 + self.assertEqual(len(mail.outbox), expected_emails_sent) + self.check_lib_email(mail.outbox[expected_emails_sent - 1], new_lib, existing_lib_user) + + @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) + def test_new_library_form_honeypot(self): + new_lib = self.new_lib() + new_lib_user = self.new_lib_user() + self.submit_form('sign_up_libraries', + data = { 'b-email': new_lib['email'], + 'b-website': new_lib['website'], + 'b-name': new_lib['name'], + 'a-e-address': new_lib_user['raw_email'], + 'a-first_name': new_lib_user['first'], + 'a-last_name': new_lib_user['last'], + 'a-telephone': "I'm a bot."}, + success_url=reverse('register_library_instructions')) + self.assertEqual(len(mail.outbox), 0) + self.assertFalse(Registrar.objects.filter(name=new_lib['name']).exists()) + + @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) + def test_new_library_submit_failure(self): + ''' + Does the library signup form submit as expected? Failures. + ''' + new_lib = self.new_lib() + existing_lib_user = { 'email': 'test_user@example.com'} + + # Not logged in, blank submission reports correct fields required + # ('email' catches both registrar and user email errors, unavoidably, + # so test with just that missing separately) + self.submit_form('sign_up_libraries', + data = {}, + form_keys = ['registrar_form', 'user_form'], + error_keys = ['website', 'name', 'email']) + self.assertEqual(len(mail.outbox), 0) + + # (checking user email missing separately) + self.submit_form('sign_up_libraries', + data = {'b-email': new_lib['email'], + 'b-website': new_lib['website'], + 'b-name': new_lib['name']}, + form_keys = ['registrar_form', 'user_form'], + error_keys = ['email']) + self.assertEqual(len(mail.outbox), 0) + + # Not logged in, user appears to have already registered + data = {'b-email': new_lib['email'], + 'b-website': new_lib['website'], + 'b-name': new_lib['name'], + 'a-e-address': self.randomize_capitalization(existing_lib_user['email'])} + self.submit_form('sign_up_libraries', + data = data, + form_keys = ['registrar_form', 'user_form'], + success_url = '/login?next=/libraries/') + self.assertDictEqual(self.client.session['request_data'], data) + self.assertEqual(len(mail.outbox), 0) + + # Not logged in, registrar appears to exist already + # (actually, this doesn't currently fail) + + # Logged in, blank submission reports all fields required + self.submit_form('sign_up_libraries', + data = {}, + user = existing_lib_user['email'], + error_keys = ['website', 'name', 'email']) + self.assertEqual(len(mail.outbox), 0) + + # Logged in, registrar appears to exist already + # (actually, this doesn't currently fail) + + ### Courts ### + + def new_court(self): + rand = random() + return { 'requested_account_note': 'Court {}'.format(rand) } + + def new_court_user(self): + rand = random() + email = self.randomize_capitalization('user{}@university.org'.format(rand)) + return { 'raw_email': email, + 'normalized_email': email.lower(), + 'first': 'Joe', + 'last': 'Yacobówski' } + + def check_court_email(self, message, court_email): + our_address = settings.DEFAULT_FROM_EMAIL + + # Doesn't check email contents yet; too many variations possible presently + self.assertEqual(message.subject, "Perma.cc new library court account information request") + self.assertEqual(message.from_email, our_address) + self.assertEqual(message.recipients(), [our_address]) + self.assertDictEqual(message.extra_headers, {'Reply-To': court_email}) + + @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) + def test_new_court_success(self): + ''' + Does the court signup form submit as expected? Success cases. + ''' + new_court = self.new_court() + new_user = self.new_court_user() + existing_user = { 'email': 'test_user@example.com'} + another_existing_user = { 'email': 'another_library_user@example.com'} + expected_emails_sent = 0 + + # NOT LOGGED IN + + # Existing user's email address, no court info + # (currently succeeds, should probably fail; see issue 1746) + self.submit_form('sign_up_courts', + data = { 'e-address': self.randomize_capitalization(existing_user['email'])}, + success_url = reverse('court_request_response')) + expected_emails_sent += 1 + self.assertEqual(len(mail.outbox), expected_emails_sent) + self.check_court_email(mail.outbox[expected_emails_sent - 1], existing_user['email']) + + # Existing user's email address + court info + self.submit_form('sign_up_courts', + data = { 'e-address': self.randomize_capitalization(existing_user['email']), + 'requested_account_note': new_court['requested_account_note']}, + success_url = reverse('court_request_response')) + expected_emails_sent += 1 + self.assertEqual(len(mail.outbox), expected_emails_sent) + self.check_court_email(mail.outbox[expected_emails_sent - 1], existing_user['email']) + + # New user email address, don't create account + self.submit_form('sign_up_courts', + data = { 'e-address': new_user['raw_email'], + 'requested_account_note': new_court['requested_account_note']}, + success_url = reverse('court_request_response')) + expected_emails_sent += 1 + self.assertEqual(len(mail.outbox), expected_emails_sent) + self.check_court_email(mail.outbox[expected_emails_sent - 1], new_user['raw_email']) + + # New user email address, create account + self.submit_form('sign_up_courts', + data = { 'e-address': new_user['raw_email'], + 'requested_account_note': new_court['requested_account_note'], + 'create_account': True }, + success_url = reverse('register_email_instructions')) + expected_emails_sent += 2 + self.assertEqual(len(mail.outbox), expected_emails_sent) + self.check_new_activation_email(mail.outbox[expected_emails_sent - 2], new_user['raw_email']) + self.check_court_email(mail.outbox[expected_emails_sent - 1], new_user['raw_email']) + + # LOGGED IN + + # New user email address + # (This succeeds and creates a new account; see issue 1749) + new_user = self.new_court_user() + self.submit_form('sign_up_courts', + data = { 'e-address': new_user['raw_email'], + 'requested_account_note': new_court['requested_account_note'], + 'create_account': True }, + user = existing_user['email'], + success_url = reverse('register_email_instructions')) + expected_emails_sent += 2 + self.assertEqual(len(mail.outbox), expected_emails_sent) + self.check_new_activation_email(mail.outbox[expected_emails_sent - 2], new_user['raw_email']) + self.check_court_email(mail.outbox[expected_emails_sent - 1], new_user['raw_email']) + + # Existing user's email address, not that of the user logged in. + # (This is odd; see issue 1749) + self.submit_form('sign_up_courts', + data = { 'e-address': self.randomize_capitalization(existing_user['email']), + 'requested_account_note': new_court['requested_account_note'], + 'create_account': True }, + user = another_existing_user['email'], + success_url = reverse('court_request_response')) + expected_emails_sent += 1 + self.assertEqual(len(mail.outbox), expected_emails_sent) + self.check_court_email(mail.outbox[expected_emails_sent - 1], existing_user['email']) + + @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) + def test_new_court_form_honeypot(self): + new_court = self.new_court() + new_user = self.new_court_user() + self.submit_form('sign_up_courts', + data = { 'e-address': new_user['raw_email'], + 'requested_account_note': new_court['requested_account_note'], + 'create_account': True, + 'telephone': "I'm a bot." }, + success_url = reverse('register_email_instructions')) + self.assertEqual(len(mail.outbox), 0) + self.assertFalse(LinkUser.objects.filter(email__iexact=new_user['raw_email']).exists()) + + @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) + def test_new_court_failure(self): + ''' + Does the court signup form submit as expected? Failure cases. + ''' + # Not logged in, blank submission reports correct fields required + self.submit_form('sign_up_courts', + data = {}, + error_keys = ['email', 'requested_account_note']) + self.assertEqual(len(mail.outbox), 0) + + # Logged in, blank submission reports same fields required + # (This is odd; see issue 1749) + self.submit_form('sign_up_courts', + data = {}, + user = 'test_user@example.com', + error_keys = ['email', 'requested_account_note']) + self.assertEqual(len(mail.outbox), 0) + + + ### Firms ### + + def create_firm_registrar_form(self): + return { + 'name': f'Firm {random()}', + 'email': 'test-firm@example.com', + 'website': 'https://www.example.com', + } + + def create_firm_usage_form(self): + return { + 'estimated_number_of_accounts': '10 - 50', + 'estimated_perma_links_per_month': '100+', + } + + def create_firm_user_form(self): + email = self.randomize_capitalization(f'user{random()}@university.org') + return { + 'raw_email': email, + 'normalized_email': email.lower(), + 'first': 'Joe', + 'last': 'Yacobówski', + 'registrar_user_candidate': bool(getrandbits(1)), + } + + def check_firm_email(self, message: str, firm_email: str): + perma_admin_email = settings.DEFAULT_FROM_EMAIL + + self.assertEqual(message.subject, 'Perma.cc new paid registrar account request') + self.assertEqual(message.from_email, perma_admin_email) + self.assertEqual(message.to, [firm_email.lower()]) + self.assertEqual(message.cc, [perma_admin_email]) + self.assertEqual(message.reply_to, [perma_admin_email]) + + @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) + def test_new_firm_success(self): + firm_registrar_form = self.create_firm_registrar_form() + firm_usage_form = self.create_firm_usage_form() + firm_user_form = self.create_firm_user_form() + existing_user = {'email': 'test_user@example.com'} + expected_emails_sent = 0 + + # NOT LOGGED IN + + # Existing user's email address, no firm info (should not succeed due to missing values) + self.submit_form( + 'sign_up_firms', + data={'a-e-address': self.randomize_capitalization(existing_user['email'])}, + ) + expected_emails_sent += 0 + self.assertEqual(len(mail.outbox), expected_emails_sent) + + # Existing user's email address + firm info + self.submit_form( + 'sign_up_firms', + data={ + 'a-e-address': self.randomize_capitalization(existing_user['email']), + 'a-registrar_user_candidate': firm_user_form['registrar_user_candidate'], + **firm_registrar_form, + **firm_usage_form, + }, + success_url=reverse('firm_request_response'), + ) + expected_emails_sent += 1 + self.assertEqual(len(mail.outbox), expected_emails_sent) + self.check_firm_email(mail.outbox[expected_emails_sent - 1], existing_user['email']) + + # New user email address, don't create account + self.submit_form( + 'sign_up_firms', + data={ + 'a-e-address': firm_user_form['raw_email'], + 'a-registrar_user_candidate': firm_user_form['registrar_user_candidate'], + **firm_registrar_form, + **firm_usage_form, + }, + success_url=reverse('firm_request_response'), + ) + expected_emails_sent += 1 + self.assertEqual(len(mail.outbox), expected_emails_sent) + self.check_firm_email(mail.outbox[expected_emails_sent - 1], firm_user_form['raw_email']) + + # New user email address, create account + self.submit_form( + 'sign_up_firms', + data={ + 'a-e-address': firm_user_form['raw_email'], + 'a-registrar_user_candidate': firm_user_form['registrar_user_candidate'], + **firm_registrar_form, + **firm_usage_form, + 'create_account': True, + }, + success_url=reverse('register_email_instructions'), + ) + expected_emails_sent += 2 + self.assertEqual(len(mail.outbox), expected_emails_sent) + self.check_firm_email(mail.outbox[expected_emails_sent - 2], firm_user_form['raw_email']) + self.check_new_activation_email( + mail.outbox[expected_emails_sent - 1], firm_user_form['raw_email'] + ) + + # LOGGED IN + + # Existing user + self.submit_form( + 'sign_up_firms', + data={ + 'a-e-address': existing_user['email'], + 'a-registrar_user_candidate': firm_user_form['registrar_user_candidate'], + **firm_registrar_form, + **firm_usage_form, + }, + user=existing_user['email'], + success_url=reverse('firm_request_response'), + ) + expected_emails_sent += 1 + self.assertEqual(len(mail.outbox), expected_emails_sent) + self.check_firm_email(mail.outbox[expected_emails_sent - 1], existing_user['email']) + + @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) + def test_new_firm_form_honeypot(self): + firm_registrar_form = self.create_firm_registrar_form() + firm_usage_form = self.create_firm_usage_form() + firm_user_form = self.create_firm_user_form() + self.submit_form( + 'sign_up_firms', + data={ + 'a-e-address': firm_user_form['raw_email'], + 'create_account': True, + 'a-telephone': "I'm a bot.", + **firm_registrar_form, + **firm_usage_form, + 'a-registrar_user_candidate': True, + }, + success_url=reverse('register_email_instructions'), + ) + self.assertEqual(len(mail.outbox), 0) + self.assertFalse( + LinkUser.objects.filter(email__iexact=firm_user_form['raw_email']).exists() + ) + + @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) + def test_new_firm_failure(self): + ''' + Does the firm signup form submit as expected? Failure cases. + ''' + error_keys = [ + 'email', + 'website', + 'estimated_number_of_accounts', + 'estimated_perma_links_per_month', + 'name', + 'registrar_user_candidate', + ] + + # Not logged in, blank submission reports correct fields required + self.submit_form( + 'sign_up_firms', + data={}, + form_keys=['registrar_form', 'usage_form', 'user_form'], + error_keys=error_keys, + ) + self.assertEqual(len(mail.outbox), 0) + + # Logged in, blank submission reports same fields required + # (This is odd; see issue 1749) + self.submit_form( + 'sign_up_firms', + data={}, + form_keys=['registrar_form', 'usage_form', 'user_form'], + user='test_user@example.com', + error_keys=error_keys, + ) + self.assertEqual(len(mail.outbox), 0) + + ### Individual Users ### + + def check_new_activation_email(self, message, user_email): + self.assertEqual(message.subject, "A Perma.cc account has been created for you") + self.assertEqual(message.from_email, settings.DEFAULT_FROM_EMAIL) + self.assertEqual(message.recipients(), [user_email]) + + activation_url = next( + line for line in message.body.rstrip().split('\n') if line.strip().startswith('http') + ) + return activation_url + + @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) + def test_account_creation_views(self): + # user registration + new_user_raw_email = self.randomize_capitalization("new_email@test.com") + new_user_normalized_email = new_user_raw_email.lower() + self.submit_form('sign_up', {'e-address': new_user_raw_email, 'first_name': 'Test', 'last_name': 'Test'}, + success_url=reverse('register_email_instructions'), + success_query=LinkUser.objects.filter( + email=new_user_normalized_email, + raw_email=new_user_raw_email + )) + + # email sent + self.assertEqual(len(mail.outbox), 1) + message = mail.outbox[0] + activation_url = self.check_new_activation_email(message, new_user_raw_email) + + # the new user is created, but is unactivated + user = LinkUser.objects.get(email=new_user_normalized_email) + self.assertEqual(user.raw_email, new_user_raw_email) + self.assertFalse(user.is_active) + self.assertFalse(user.is_confirmed) + + # if you tamper with the code, it is rejected + response = self.client.get(activation_url[:-1]+'wrong/', secure=True) + self.assertContains(response, 'This activation/reset link is invalid') + + # reg confirm - non-matching passwords + response = self.client.get(activation_url, follow=True, secure=True) + post_url = response.redirect_chain[0][0] + self.assertTemplateUsed(response, 'registration/password_reset_confirm.html') + response = self.client.post(post_url, {'new_password1': 'Anewpass1', 'new_password2': 'Anewpass2'}, follow=True, secure=True) + self.assertNotContains(response, 'Your password has been set') + self.assertContains(response, "The two password fields didn’t match") + # reg confirm - correct + response = self.client.post(post_url, {'new_password1': 'Anewpass1', 'new_password2': 'Anewpass1'}, follow=True, secure=True) + self.assertContains(response, 'Your password has been set') + + # Doesn't work twice. + response = self.client.post(post_url, {'new_password1': 'Anotherpass1', 'new_password2': 'Anotherpass1'}, follow=True, secure=True) + self.assertContains(response, 'This activation/reset link is invalid') + + # the new user is now activated and can log in + user.refresh_from_db() + self.assertTrue(user.is_active) + self.assertTrue(user.is_confirmed) + response = self.client.post(reverse('user_management_limited_login'), {'username': new_user_raw_email, 'password': 'Anewpass1'}, follow=True, secure=True) + self.assertEqual(response.redirect_chain[0][0], '/manage/create/') + + @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) + def test_suggested_registrars(self): + # Register user + _, registrar_domain = self.registrar.email.split('@') + new_user_email = f'new_user@{registrar_domain}' + self.submit_form( + 'sign_up', + {'e-address': new_user_email, 'first_name': 'Test', 'last_name': 'Test'}, + success_url=reverse('register_email_instructions'), + success_query=LinkUser.objects.filter(email=new_user_email), + ) + self.assertEqual(len(mail.outbox), 1) + + # Obtain suggested registrar(s) from activation email message + message = mail.outbox[0] + lines = message.body.splitlines() + captures = [] + for line in lines: + if line.lstrip().startswith('- '): + captures.append(line.strip('- ')) + + # Validate suggested registrar(s) + self.assertEqual(len(captures), 1) + self.assertEqual(captures[0], f'{self.registrar.name}: {self.registrar.email}') + + @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) + def test_signup_with_existing_email_rejected(self): + self.assertEqual(LinkUser.objects.filter(email__iexact=self.registrar_user.email).count(), 1) + self.submit_form('sign_up', + {'e-address': self.registrar_user.email, 'first_name': 'Test', 'last_name': 'Test'}, + error_keys=['email']) + self.submit_form('sign_up', + {'e-address': self.randomize_capitalization(self.registrar_user.email), 'first_name': 'Test', 'last_name': 'Test'}, + error_keys=['email']) + self.assertEqual(LinkUser.objects.filter(email__iexact=self.registrar_user.email).count(), 1) + + @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) + def test_new_user_form_honeypot(self): + new_user_email = "new_email@test.com" + self.submit_form('sign_up', + data = { 'e-address': new_user_email, + 'telephone': "I'm a bot." }, + success_url = reverse('register_email_instructions')) + self.assertEqual(len(mail.outbox), 0) + self.assertFalse(LinkUser.objects.filter(email__iexact=new_user_email).exists()) + + def test_manual_user_creation_rejects_duplicative_emails(self): + email = 'test_user@example.com' + self.assertTrue(LinkUser.objects.filter(email=email).exists()) + new_user = LinkUser(email=self.randomize_capitalization(email)) + self.assertRaises(IntegrityError, new_user.save) + + def test_get_new_activation_code(self): + self.submit_form('user_management_not_active', + user = 'unactivated_faculty_user@example.com', + data = {}, + success_url=reverse('user_management_limited_login')) + self.assertEqual(len(mail.outbox), 1) + self.check_new_activation_email(mail.outbox[0], 'unactivated_faculty_user@example.com') + + + ### PASSWORD RESETS ### + + def test_password_reset_is_case_insensitive(self): + email = 'test_user@example.com' + not_a_user = 'doesnotexist@example.com' + self.assertEqual(LinkUser.objects.filter(email__iexact=email).count(), 1) + self.assertFalse(LinkUser.objects.filter(email=not_a_user).exists()) + + self.submit_form('password_reset', data={}) + self.submit_form('password_reset', data={'email': not_a_user}) + self.assertEqual(len(mail.outbox), 0) + + self.submit_form('password_reset', data={'email': email}) + self.assertEqual(len(mail.outbox), 1) + + self.submit_form('password_reset', data={'email': self.randomize_capitalization(email)}) + self.assertEqual(len(mail.outbox), 2) + + diff --git a/perma_web/perma/tests/test_views_user_management.py b/perma_web/perma/tests/test_views_user_management.py index 9ae1b3862..445eec520 100644 --- a/perma_web/perma/tests/test_views_user_management.py +++ b/perma_web/perma/tests/test_views_user_management.py @@ -4,15 +4,11 @@ from io import StringIO import json import pytest -from random import random, getrandbits + import re from bs4 import BeautifulSoup from django.urls import reverse -from django.core import mail -from django.conf import settings -from django.db import IntegrityError -from django.test import override_settings from perma.models import LinkUser, Organization, Registrar, UserOrganizationAffiliation from perma.tests.utils import PermaTestCase @@ -2130,29 +2126,7 @@ class UserManagementViewsTestCase(PermaTestCase): @classmethod def setUpTestData(cls): cls.admin_user = LinkUser.objects.get(pk=1) - cls.registrar_user = LinkUser.objects.get(pk=2) - cls.sponsored_user = LinkUser.objects.get(pk=20) - cls.another_sponsored_user = LinkUser.objects.get(pk=21) - cls.inactive_sponsored_user = LinkUser.objects.get(pk=22) - cls.another_inactive_sponsored_user = LinkUser.objects.get(pk=23) - cls.regular_user = LinkUser.objects.get(pk=4) - cls.another_regular_user = LinkUser.objects.get(pk=16) - cls.registrar = cls.registrar_user.registrar - cls.pending_registrar = Registrar.objects.get(pk=2) - cls.unrelated_registrar = Registrar.objects.get(pk=2) - cls.unrelated_registrar_user = cls.unrelated_registrar.users.first() - cls.organization = Organization.objects.get(pk=1) - cls.user_organization_affiliation = UserOrganizationAffiliation.objects.get(pk=1) - cls.organization_user = cls.organization.users.first() - cls.another_organization = Organization.objects.get(pk=2) - cls.unrelated_organization = cls.unrelated_registrar.organizations.first() - cls.unrelated_organization_user = cls.unrelated_organization.users.first() - cls.another_unrelated_organization_user = cls.unrelated_organization.users.get(pk=11) - cls.deletable_organization = Organization.objects.get(pk=3) - - ### Helpers ### - def pk_from_email(self, email): - return LinkUser.objects.get(email=email).pk + ### REGISTRAR A/E/D VIEWS ### @@ -2449,695 +2423,3 @@ def test_user_list_filters(self): self.assertEqual(response.count(b'Interested in a faculty account'), 1) # status filter tested in test_registrar_user_list_filters - - - ### - ### SIGNUP - ### - - ### Libraries ### - - def new_lib(self): - rand = random() - return { 'email': 'library{}@university.org'.format(rand), - 'name': 'University Library {}'.format(rand), - 'website': 'http://website{}.org'.format(rand), - 'address': '{} Main St., Boston MA 02144'.format(rand)} - - def new_lib_user(self): - rand = random() - email = self.randomize_capitalization('user{}@university.org'.format(rand)) - return { 'raw_email': email, - 'normalized_email': email.lower(), - 'first': 'Joe', - 'last': 'Yacobówski' } - - def check_library_labels(self, soup): - name_label = soup.find('label', {'for': 'id_b-name'}) - self.assertEqual(name_label.text, "Library name") - email_label = soup.find('label', {'for': 'id_b-email'}) - self.assertEqual(email_label.text, "Library email") - website_label = soup.find('label', {'for': 'id_b-website'}) - self.assertEqual(website_label.text, "Library website") - - def check_lib_user_labels(self, soup): - email_label = soup.find('label', {'for': 'id_a-e-address'}) - self.assertEqual(email_label.text, "Your email") - - def check_lib_email(self, message, new_lib, user): - our_address = settings.DEFAULT_FROM_EMAIL - - self.assertIn(new_lib['name'], message.body) - self.assertIn(new_lib['email'], message.body) - - self.assertIn(user['raw_email'], message.body) - - id = Registrar.objects.get(email=new_lib['email']).id - approve_url = "http://testserver{}".format(reverse('user_sign_up_approve_pending_registrar', args=[id])) - self.assertIn(approve_url, message.body) - self.assertEqual(message.subject, "Perma.cc new library registrar account request") - self.assertEqual(message.from_email, our_address) - self.assertEqual(message.recipients(), [our_address]) - self.assertDictEqual(message.extra_headers, {'Reply-To': user['raw_email']}) - - @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) - def test_new_library_render(self): - ''' - Does the library signup form display as expected? - ''' - - # NOT LOGGED IN - - # Registrar and user forms are displayed, - # inputs are blank, and labels are customized as expected - response = self.get('sign_up_libraries').content - soup = BeautifulSoup(response, 'html.parser') - self.check_library_labels(soup) - self.check_lib_user_labels(soup) - inputs = soup.select('input') - self.assertEqual(len(inputs), 9) - for input in inputs: - if input['name'] in ['csrfmiddlewaretoken', 'telephone']: - self.assertTrue(input.get('value', '')) - else: - self.assertFalse(input.get('value', '')) - - # If request_data is present in session, registrar form is prepopulated, - # and labels are still customized as expected - session = self.client.session - new_lib = self.new_lib() - new_lib_user = self.new_lib_user() - session['request_data'] = { 'b-email': new_lib['email'], - 'b-website': new_lib['website'], - 'b-name': new_lib['name'], - 'b-address': new_lib['address'], - 'a-e-address': new_lib_user['raw_email'], - 'a-first_name': new_lib_user['first'], - 'a-last_name': new_lib_user['last'], - 'csrfmiddlewaretoken': '11YY3S2DgOw2DHoWVEbBArnBMdEA2svu' } - session.save() - response = self.get('sign_up_libraries').content - soup = BeautifulSoup(response, 'html.parser') - self.check_library_labels(soup) - self.check_lib_user_labels(soup) - inputs = soup.select('input') - self.assertEqual(len(inputs), 9) - for input in inputs: - if input['name'] in ['csrfmiddlewaretoken', 'telephone']: - self.assertTrue(input.get('value', '')) - elif input['name'][:2] == "b-": - self.assertTrue(input.get('value', '')) - else: - self.assertFalse(input.get('value', '')) - - # If there's an unsuccessful submission, field labels are still as expected. - response = self.post('sign_up_libraries').content - soup = BeautifulSoup(response, 'html.parser') - self.check_library_labels(soup) - self.check_lib_user_labels(soup) - - # LOGGED IN - - # Registrar form is displayed, but user form is not, - # inputs are blank, and labels are still customized as expected - response = self.get('sign_up_libraries', user="test_user@example.com").content - soup = BeautifulSoup(response, 'html.parser') - self.check_library_labels(soup) - inputs = soup.select('input') - self.assertEqual(len(inputs), 6) # 6 because csrf is here and in the logout form - for input in inputs: - self.assertIn(input['name'],['csrfmiddlewaretoken', 'b-name', 'b-email', 'b-website', 'b-address']) - if input['name'] == 'csrfmiddlewaretoken': - self.assertTrue(input.get('value', '')) - else: - self.assertFalse(input.get('value', '')) - - @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) - def test_new_library_submit_success(self): - ''' - Does the library signup form submit as expected? Success cases. - ''' - expected_emails_sent = 0 - - # Not logged in, submit all fields sans first and last name - new_lib = self.new_lib() - new_lib_user = self.new_lib_user() - self.submit_form('sign_up_libraries', - data = { 'b-email': new_lib['email'], - 'b-website': new_lib['website'], - 'b-name': new_lib['name'], - 'a-e-address': new_lib_user['raw_email'] }, - success_url=reverse('register_library_instructions')) - expected_emails_sent += 2 - self.assertEqual(len(mail.outbox), expected_emails_sent) - self.check_lib_email(mail.outbox[expected_emails_sent - 2], new_lib, new_lib_user) - self.check_new_activation_email(mail.outbox[expected_emails_sent - 1], new_lib_user['raw_email']) - - # Not logged in, submit all fields including first and last name - new_lib = self.new_lib() - new_lib_user = self.new_lib_user() - self.submit_form('sign_up_libraries', - data = { 'b-email': new_lib['email'], - 'b-website': new_lib['website'], - 'b-name': new_lib['name'], - 'a-e-address': new_lib_user['raw_email'], - 'a-first_name': new_lib_user['first'], - 'a-last_name': new_lib_user['last']}, - success_url=reverse('register_library_instructions')) - expected_emails_sent += 2 - self.assertEqual(len(mail.outbox), expected_emails_sent) - self.check_lib_email(mail.outbox[expected_emails_sent - 2], new_lib, new_lib_user) - self.check_new_activation_email(mail.outbox[expected_emails_sent - 1], new_lib_user['raw_email']) - - # Logged in - new_lib = self.new_lib() - existing_lib_user = { - 'raw_email': 'test_user@example.com', - 'normalized_email': 'test_user@example.com', - } - self.submit_form('sign_up_libraries', - data = { 'b-email': new_lib['email'], - 'b-website': new_lib['website'], - 'b-name': new_lib['name'] }, - success_url=reverse('settings_affiliations'), - user=existing_lib_user['raw_email']) - expected_emails_sent += 1 - self.assertEqual(len(mail.outbox), expected_emails_sent) - self.check_lib_email(mail.outbox[expected_emails_sent - 1], new_lib, existing_lib_user) - - @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) - def test_new_library_form_honeypot(self): - new_lib = self.new_lib() - new_lib_user = self.new_lib_user() - self.submit_form('sign_up_libraries', - data = { 'b-email': new_lib['email'], - 'b-website': new_lib['website'], - 'b-name': new_lib['name'], - 'a-e-address': new_lib_user['raw_email'], - 'a-first_name': new_lib_user['first'], - 'a-last_name': new_lib_user['last'], - 'a-telephone': "I'm a bot."}, - success_url=reverse('register_library_instructions')) - self.assertEqual(len(mail.outbox), 0) - self.assertFalse(Registrar.objects.filter(name=new_lib['name']).exists()) - - @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) - def test_new_library_submit_failure(self): - ''' - Does the library signup form submit as expected? Failures. - ''' - new_lib = self.new_lib() - existing_lib_user = { 'email': 'test_user@example.com'} - - # Not logged in, blank submission reports correct fields required - # ('email' catches both registrar and user email errors, unavoidably, - # so test with just that missing separately) - self.submit_form('sign_up_libraries', - data = {}, - form_keys = ['registrar_form', 'user_form'], - error_keys = ['website', 'name', 'email']) - self.assertEqual(len(mail.outbox), 0) - - # (checking user email missing separately) - self.submit_form('sign_up_libraries', - data = {'b-email': new_lib['email'], - 'b-website': new_lib['website'], - 'b-name': new_lib['name']}, - form_keys = ['registrar_form', 'user_form'], - error_keys = ['email']) - self.assertEqual(len(mail.outbox), 0) - - # Not logged in, user appears to have already registered - data = {'b-email': new_lib['email'], - 'b-website': new_lib['website'], - 'b-name': new_lib['name'], - 'a-e-address': self.randomize_capitalization(existing_lib_user['email'])} - self.submit_form('sign_up_libraries', - data = data, - form_keys = ['registrar_form', 'user_form'], - success_url = '/login?next=/libraries/') - self.assertDictEqual(self.client.session['request_data'], data) - self.assertEqual(len(mail.outbox), 0) - - # Not logged in, registrar appears to exist already - # (actually, this doesn't currently fail) - - # Logged in, blank submission reports all fields required - self.submit_form('sign_up_libraries', - data = {}, - user = existing_lib_user['email'], - error_keys = ['website', 'name', 'email']) - self.assertEqual(len(mail.outbox), 0) - - # Logged in, registrar appears to exist already - # (actually, this doesn't currently fail) - - ### Courts ### - - def new_court(self): - rand = random() - return { 'requested_account_note': 'Court {}'.format(rand) } - - def new_court_user(self): - rand = random() - email = self.randomize_capitalization('user{}@university.org'.format(rand)) - return { 'raw_email': email, - 'normalized_email': email.lower(), - 'first': 'Joe', - 'last': 'Yacobówski' } - - def check_court_email(self, message, court_email): - our_address = settings.DEFAULT_FROM_EMAIL - - # Doesn't check email contents yet; too many variations possible presently - self.assertEqual(message.subject, "Perma.cc new library court account information request") - self.assertEqual(message.from_email, our_address) - self.assertEqual(message.recipients(), [our_address]) - self.assertDictEqual(message.extra_headers, {'Reply-To': court_email}) - - @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) - def test_new_court_success(self): - ''' - Does the court signup form submit as expected? Success cases. - ''' - new_court = self.new_court() - new_user = self.new_court_user() - existing_user = { 'email': 'test_user@example.com'} - another_existing_user = { 'email': 'another_library_user@example.com'} - expected_emails_sent = 0 - - # NOT LOGGED IN - - # Existing user's email address, no court info - # (currently succeeds, should probably fail; see issue 1746) - self.submit_form('sign_up_courts', - data = { 'e-address': self.randomize_capitalization(existing_user['email'])}, - success_url = reverse('court_request_response')) - expected_emails_sent += 1 - self.assertEqual(len(mail.outbox), expected_emails_sent) - self.check_court_email(mail.outbox[expected_emails_sent - 1], existing_user['email']) - - # Existing user's email address + court info - self.submit_form('sign_up_courts', - data = { 'e-address': self.randomize_capitalization(existing_user['email']), - 'requested_account_note': new_court['requested_account_note']}, - success_url = reverse('court_request_response')) - expected_emails_sent += 1 - self.assertEqual(len(mail.outbox), expected_emails_sent) - self.check_court_email(mail.outbox[expected_emails_sent - 1], existing_user['email']) - - # New user email address, don't create account - self.submit_form('sign_up_courts', - data = { 'e-address': new_user['raw_email'], - 'requested_account_note': new_court['requested_account_note']}, - success_url = reverse('court_request_response')) - expected_emails_sent += 1 - self.assertEqual(len(mail.outbox), expected_emails_sent) - self.check_court_email(mail.outbox[expected_emails_sent - 1], new_user['raw_email']) - - # New user email address, create account - self.submit_form('sign_up_courts', - data = { 'e-address': new_user['raw_email'], - 'requested_account_note': new_court['requested_account_note'], - 'create_account': True }, - success_url = reverse('register_email_instructions')) - expected_emails_sent += 2 - self.assertEqual(len(mail.outbox), expected_emails_sent) - self.check_new_activation_email(mail.outbox[expected_emails_sent - 2], new_user['raw_email']) - self.check_court_email(mail.outbox[expected_emails_sent - 1], new_user['raw_email']) - - # LOGGED IN - - # New user email address - # (This succeeds and creates a new account; see issue 1749) - new_user = self.new_court_user() - self.submit_form('sign_up_courts', - data = { 'e-address': new_user['raw_email'], - 'requested_account_note': new_court['requested_account_note'], - 'create_account': True }, - user = existing_user['email'], - success_url = reverse('register_email_instructions')) - expected_emails_sent += 2 - self.assertEqual(len(mail.outbox), expected_emails_sent) - self.check_new_activation_email(mail.outbox[expected_emails_sent - 2], new_user['raw_email']) - self.check_court_email(mail.outbox[expected_emails_sent - 1], new_user['raw_email']) - - # Existing user's email address, not that of the user logged in. - # (This is odd; see issue 1749) - self.submit_form('sign_up_courts', - data = { 'e-address': self.randomize_capitalization(existing_user['email']), - 'requested_account_note': new_court['requested_account_note'], - 'create_account': True }, - user = another_existing_user['email'], - success_url = reverse('court_request_response')) - expected_emails_sent += 1 - self.assertEqual(len(mail.outbox), expected_emails_sent) - self.check_court_email(mail.outbox[expected_emails_sent - 1], existing_user['email']) - - @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) - def test_new_court_form_honeypot(self): - new_court = self.new_court() - new_user = self.new_court_user() - self.submit_form('sign_up_courts', - data = { 'e-address': new_user['raw_email'], - 'requested_account_note': new_court['requested_account_note'], - 'create_account': True, - 'telephone': "I'm a bot." }, - success_url = reverse('register_email_instructions')) - self.assertEqual(len(mail.outbox), 0) - self.assertFalse(LinkUser.objects.filter(email__iexact=new_user['raw_email']).exists()) - - @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) - def test_new_court_failure(self): - ''' - Does the court signup form submit as expected? Failure cases. - ''' - # Not logged in, blank submission reports correct fields required - self.submit_form('sign_up_courts', - data = {}, - error_keys = ['email', 'requested_account_note']) - self.assertEqual(len(mail.outbox), 0) - - # Logged in, blank submission reports same fields required - # (This is odd; see issue 1749) - self.submit_form('sign_up_courts', - data = {}, - user = 'test_user@example.com', - error_keys = ['email', 'requested_account_note']) - self.assertEqual(len(mail.outbox), 0) - - - ### Firms ### - - def create_firm_registrar_form(self): - return { - 'name': f'Firm {random()}', - 'email': 'test-firm@example.com', - 'website': 'https://www.example.com', - } - - def create_firm_usage_form(self): - return { - 'estimated_number_of_accounts': '10 - 50', - 'estimated_perma_links_per_month': '100+', - } - - def create_firm_user_form(self): - email = self.randomize_capitalization(f'user{random()}@university.org') - return { - 'raw_email': email, - 'normalized_email': email.lower(), - 'first': 'Joe', - 'last': 'Yacobówski', - 'registrar_user_candidate': bool(getrandbits(1)), - } - - def check_firm_email(self, message: str, firm_email: str): - perma_admin_email = settings.DEFAULT_FROM_EMAIL - - self.assertEqual(message.subject, 'Perma.cc new paid registrar account request') - self.assertEqual(message.from_email, perma_admin_email) - self.assertEqual(message.to, [firm_email.lower()]) - self.assertEqual(message.cc, [perma_admin_email]) - self.assertEqual(message.reply_to, [perma_admin_email]) - - @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) - def test_new_firm_success(self): - firm_registrar_form = self.create_firm_registrar_form() - firm_usage_form = self.create_firm_usage_form() - firm_user_form = self.create_firm_user_form() - existing_user = {'email': 'test_user@example.com'} - expected_emails_sent = 0 - - # NOT LOGGED IN - - # Existing user's email address, no firm info (should not succeed due to missing values) - self.submit_form( - 'sign_up_firms', - data={'a-e-address': self.randomize_capitalization(existing_user['email'])}, - ) - expected_emails_sent += 0 - self.assertEqual(len(mail.outbox), expected_emails_sent) - - # Existing user's email address + firm info - self.submit_form( - 'sign_up_firms', - data={ - 'a-e-address': self.randomize_capitalization(existing_user['email']), - 'a-registrar_user_candidate': firm_user_form['registrar_user_candidate'], - **firm_registrar_form, - **firm_usage_form, - }, - success_url=reverse('firm_request_response'), - ) - expected_emails_sent += 1 - self.assertEqual(len(mail.outbox), expected_emails_sent) - self.check_firm_email(mail.outbox[expected_emails_sent - 1], existing_user['email']) - - # New user email address, don't create account - self.submit_form( - 'sign_up_firms', - data={ - 'a-e-address': firm_user_form['raw_email'], - 'a-registrar_user_candidate': firm_user_form['registrar_user_candidate'], - **firm_registrar_form, - **firm_usage_form, - }, - success_url=reverse('firm_request_response'), - ) - expected_emails_sent += 1 - self.assertEqual(len(mail.outbox), expected_emails_sent) - self.check_firm_email(mail.outbox[expected_emails_sent - 1], firm_user_form['raw_email']) - - # New user email address, create account - self.submit_form( - 'sign_up_firms', - data={ - 'a-e-address': firm_user_form['raw_email'], - 'a-registrar_user_candidate': firm_user_form['registrar_user_candidate'], - **firm_registrar_form, - **firm_usage_form, - 'create_account': True, - }, - success_url=reverse('register_email_instructions'), - ) - expected_emails_sent += 2 - self.assertEqual(len(mail.outbox), expected_emails_sent) - self.check_firm_email(mail.outbox[expected_emails_sent - 2], firm_user_form['raw_email']) - self.check_new_activation_email( - mail.outbox[expected_emails_sent - 1], firm_user_form['raw_email'] - ) - - # LOGGED IN - - # Existing user - self.submit_form( - 'sign_up_firms', - data={ - 'a-e-address': existing_user['email'], - 'a-registrar_user_candidate': firm_user_form['registrar_user_candidate'], - **firm_registrar_form, - **firm_usage_form, - }, - user=existing_user['email'], - success_url=reverse('firm_request_response'), - ) - expected_emails_sent += 1 - self.assertEqual(len(mail.outbox), expected_emails_sent) - self.check_firm_email(mail.outbox[expected_emails_sent - 1], existing_user['email']) - - @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) - def test_new_firm_form_honeypot(self): - firm_registrar_form = self.create_firm_registrar_form() - firm_usage_form = self.create_firm_usage_form() - firm_user_form = self.create_firm_user_form() - self.submit_form( - 'sign_up_firms', - data={ - 'a-e-address': firm_user_form['raw_email'], - 'create_account': True, - 'a-telephone': "I'm a bot.", - **firm_registrar_form, - **firm_usage_form, - 'a-registrar_user_candidate': True, - }, - success_url=reverse('register_email_instructions'), - ) - self.assertEqual(len(mail.outbox), 0) - self.assertFalse( - LinkUser.objects.filter(email__iexact=firm_user_form['raw_email']).exists() - ) - - @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) - def test_new_firm_failure(self): - ''' - Does the firm signup form submit as expected? Failure cases. - ''' - error_keys = [ - 'email', - 'website', - 'estimated_number_of_accounts', - 'estimated_perma_links_per_month', - 'name', - 'registrar_user_candidate', - ] - - # Not logged in, blank submission reports correct fields required - self.submit_form( - 'sign_up_firms', - data={}, - form_keys=['registrar_form', 'usage_form', 'user_form'], - error_keys=error_keys, - ) - self.assertEqual(len(mail.outbox), 0) - - # Logged in, blank submission reports same fields required - # (This is odd; see issue 1749) - self.submit_form( - 'sign_up_firms', - data={}, - form_keys=['registrar_form', 'usage_form', 'user_form'], - user='test_user@example.com', - error_keys=error_keys, - ) - self.assertEqual(len(mail.outbox), 0) - - ### Individual Users ### - - def check_new_activation_email(self, message, user_email): - self.assertEqual(message.subject, "A Perma.cc account has been created for you") - self.assertEqual(message.from_email, settings.DEFAULT_FROM_EMAIL) - self.assertEqual(message.recipients(), [user_email]) - - activation_url = next( - line for line in message.body.rstrip().split('\n') if line.strip().startswith('http') - ) - return activation_url - - @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) - def test_account_creation_views(self): - # user registration - new_user_raw_email = self.randomize_capitalization("new_email@test.com") - new_user_normalized_email = new_user_raw_email.lower() - self.submit_form('sign_up', {'e-address': new_user_raw_email, 'first_name': 'Test', 'last_name': 'Test'}, - success_url=reverse('register_email_instructions'), - success_query=LinkUser.objects.filter( - email=new_user_normalized_email, - raw_email=new_user_raw_email - )) - - # email sent - self.assertEqual(len(mail.outbox), 1) - message = mail.outbox[0] - activation_url = self.check_new_activation_email(message, new_user_raw_email) - - # the new user is created, but is unactivated - user = LinkUser.objects.get(email=new_user_normalized_email) - self.assertEqual(user.raw_email, new_user_raw_email) - self.assertFalse(user.is_active) - self.assertFalse(user.is_confirmed) - - # if you tamper with the code, it is rejected - response = self.client.get(activation_url[:-1]+'wrong/', secure=True) - self.assertContains(response, 'This activation/reset link is invalid') - - # reg confirm - non-matching passwords - response = self.client.get(activation_url, follow=True, secure=True) - post_url = response.redirect_chain[0][0] - self.assertTemplateUsed(response, 'registration/password_reset_confirm.html') - response = self.client.post(post_url, {'new_password1': 'Anewpass1', 'new_password2': 'Anewpass2'}, follow=True, secure=True) - self.assertNotContains(response, 'Your password has been set') - self.assertContains(response, "The two password fields didn’t match") - # reg confirm - correct - response = self.client.post(post_url, {'new_password1': 'Anewpass1', 'new_password2': 'Anewpass1'}, follow=True, secure=True) - self.assertContains(response, 'Your password has been set') - - # Doesn't work twice. - response = self.client.post(post_url, {'new_password1': 'Anotherpass1', 'new_password2': 'Anotherpass1'}, follow=True, secure=True) - self.assertContains(response, 'This activation/reset link is invalid') - - # the new user is now activated and can log in - user.refresh_from_db() - self.assertTrue(user.is_active) - self.assertTrue(user.is_confirmed) - response = self.client.post(reverse('user_management_limited_login'), {'username': new_user_raw_email, 'password': 'Anewpass1'}, follow=True, secure=True) - self.assertEqual(response.redirect_chain[0][0], '/manage/create/') - - @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) - def test_suggested_registrars(self): - # Register user - _, registrar_domain = self.registrar.email.split('@') - new_user_email = f'new_user@{registrar_domain}' - self.submit_form( - 'sign_up', - {'e-address': new_user_email, 'first_name': 'Test', 'last_name': 'Test'}, - success_url=reverse('register_email_instructions'), - success_query=LinkUser.objects.filter(email=new_user_email), - ) - self.assertEqual(len(mail.outbox), 1) - - # Obtain suggested registrar(s) from activation email message - message = mail.outbox[0] - lines = message.body.splitlines() - captures = [] - for line in lines: - if line.lstrip().startswith('- '): - captures.append(line.strip('- ')) - - # Validate suggested registrar(s) - self.assertEqual(len(captures), 1) - self.assertEqual(captures[0], f'{self.registrar.name}: {self.registrar.email}') - - @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) - def test_signup_with_existing_email_rejected(self): - self.assertEqual(LinkUser.objects.filter(email__iexact=self.registrar_user.email).count(), 1) - self.submit_form('sign_up', - {'e-address': self.registrar_user.email, 'first_name': 'Test', 'last_name': 'Test'}, - error_keys=['email']) - self.submit_form('sign_up', - {'e-address': self.randomize_capitalization(self.registrar_user.email), 'first_name': 'Test', 'last_name': 'Test'}, - error_keys=['email']) - self.assertEqual(LinkUser.objects.filter(email__iexact=self.registrar_user.email).count(), 1) - - @override_settings(REQUIRE_JS_FORM_SUBMISSIONS=False) - def test_new_user_form_honeypot(self): - new_user_email = "new_email@test.com" - self.submit_form('sign_up', - data = { 'e-address': new_user_email, - 'telephone': "I'm a bot." }, - success_url = reverse('register_email_instructions')) - self.assertEqual(len(mail.outbox), 0) - self.assertFalse(LinkUser.objects.filter(email__iexact=new_user_email).exists()) - - def test_manual_user_creation_rejects_duplicative_emails(self): - email = 'test_user@example.com' - self.assertTrue(LinkUser.objects.filter(email=email).exists()) - new_user = LinkUser(email=self.randomize_capitalization(email)) - self.assertRaises(IntegrityError, new_user.save) - - def test_get_new_activation_code(self): - self.submit_form('user_management_not_active', - user = 'unactivated_faculty_user@example.com', - data = {}, - success_url=reverse('user_management_limited_login')) - self.assertEqual(len(mail.outbox), 1) - self.check_new_activation_email(mail.outbox[0], 'unactivated_faculty_user@example.com') - - - ### PASSWORD RESETS ### - - def test_password_reset_is_case_insensitive(self): - email = 'test_user@example.com' - not_a_user = 'doesnotexist@example.com' - self.assertEqual(LinkUser.objects.filter(email__iexact=email).count(), 1) - self.assertFalse(LinkUser.objects.filter(email=not_a_user).exists()) - - self.submit_form('password_reset', data={}) - self.submit_form('password_reset', data={'email': not_a_user}) - self.assertEqual(len(mail.outbox), 0) - - self.submit_form('password_reset', data={'email': email}) - self.assertEqual(len(mail.outbox), 1) - - self.submit_form('password_reset', data={'email': self.randomize_capitalization(email)}) - self.assertEqual(len(mail.outbox), 2)