FossilRepo

fossilrepo / tests / test_user_management.py
Source Blame History 339 lines
c588255… ragelink 1 import pytest
c588255… ragelink 2 from django.contrib.auth.models import User
c588255… ragelink 3 from django.test import Client
c588255… ragelink 4 from django.urls import reverse
c588255… ragelink 5
c588255… ragelink 6 from organization.models import OrganizationMember
c588255… ragelink 7
c588255… ragelink 8
c588255… ragelink 9 @pytest.fixture
c588255… ragelink 10 def org_admin_user(db, org):
c588255… ragelink 11 """A non-superuser who has ORGANIZATION_CHANGE permission via group."""
c588255… ragelink 12 from django.contrib.auth.models import Group, Permission
c588255… ragelink 13
c588255… ragelink 14 user = User.objects.create_user(username="orgadmin", email="[email protected]", password="testpass123")
c588255… ragelink 15 group, _ = Group.objects.get_or_create(name="OrgAdmins")
c588255… ragelink 16 change_perm = Permission.objects.get(content_type__app_label="organization", codename="change_organization")
c588255… ragelink 17 view_perm = Permission.objects.get(content_type__app_label="organization", codename="view_organizationmember")
c588255… ragelink 18 group.permissions.add(change_perm, view_perm)
c588255… ragelink 19 user.groups.add(group)
c588255… ragelink 20 OrganizationMember.objects.create(member=user, organization=org)
c588255… ragelink 21 return user
c588255… ragelink 22
c588255… ragelink 23
c588255… ragelink 24 @pytest.fixture
c588255… ragelink 25 def org_admin_client(org_admin_user):
c588255… ragelink 26 c = Client()
c588255… ragelink 27 c.login(username="orgadmin", password="testpass123")
c588255… ragelink 28 return c
c588255… ragelink 29
c588255… ragelink 30
c588255… ragelink 31 @pytest.fixture
c588255… ragelink 32 def target_user(db, org, admin_user):
c588255… ragelink 33 """A regular user who is an org member, to be the target of management actions."""
c588255… ragelink 34 user = User.objects.create_user(
c588255… ragelink 35 username="targetuser", email="[email protected]", password="testpass123", first_name="Target", last_name="User"
c588255… ragelink 36 )
c588255… ragelink 37 OrganizationMember.objects.create(member=user, organization=org, created_by=admin_user)
c588255… ragelink 38 return user
c588255… ragelink 39
c588255… ragelink 40
c588255… ragelink 41 # --- user_create ---
c588255… ragelink 42
c588255… ragelink 43
c588255… ragelink 44 @pytest.mark.django_db
c588255… ragelink 45 class TestUserCreate:
c588255… ragelink 46 def test_get_form(self, admin_client):
c588255… ragelink 47 response = admin_client.get(reverse("organization:user_create"))
c588255… ragelink 48 assert response.status_code == 200
c588255… ragelink 49 assert "New User" in response.content.decode()
c588255… ragelink 50
c588255… ragelink 51 def test_create_user(self, admin_client, org):
c588255… ragelink 52 response = admin_client.post(
c588255… ragelink 53 reverse("organization:user_create"),
c588255… ragelink 54 {
c588255… ragelink 55 "username": "newuser",
c588255… ragelink 56 "email": "[email protected]",
c588255… ragelink 57 "first_name": "New",
c588255… ragelink 58 "last_name": "User",
c588255… ragelink 59 "password1": "Str0ng!Pass99",
c588255… ragelink 60 "password2": "Str0ng!Pass99",
c588255… ragelink 61 },
c588255… ragelink 62 )
c588255… ragelink 63 assert response.status_code == 302
c588255… ragelink 64 user = User.objects.get(username="newuser")
c588255… ragelink 65 assert user.email == "[email protected]"
c588255… ragelink 66 assert user.first_name == "New"
c588255… ragelink 67 assert user.check_password("Str0ng!Pass99")
c588255… ragelink 68 # Verify auto-added as org member
c588255… ragelink 69 assert OrganizationMember.objects.filter(member=user, organization=org, deleted_at__isnull=True).exists()
c588255… ragelink 70
c588255… ragelink 71 def test_create_password_mismatch(self, admin_client):
c588255… ragelink 72 response = admin_client.post(
c588255… ragelink 73 reverse("organization:user_create"),
c588255… ragelink 74 {
c588255… ragelink 75 "username": "baduser",
c588255… ragelink 76 "email": "[email protected]",
c588255… ragelink 77 "password1": "Str0ng!Pass99",
c588255… ragelink 78 "password2": "differentpass",
c588255… ragelink 79 },
c588255… ragelink 80 )
c588255… ragelink 81 assert response.status_code == 200
c588255… ragelink 82 assert not User.objects.filter(username="baduser").exists()
c588255… ragelink 83
c588255… ragelink 84 def test_create_duplicate_username(self, admin_client, target_user):
c588255… ragelink 85 response = admin_client.post(
c588255… ragelink 86 reverse("organization:user_create"),
c588255… ragelink 87 {
c588255… ragelink 88 "username": "targetuser",
c588255… ragelink 89 "email": "[email protected]",
c588255… ragelink 90 "password1": "Str0ng!Pass99",
c588255… ragelink 91 "password2": "Str0ng!Pass99",
c588255… ragelink 92 },
c588255… ragelink 93 )
c588255… ragelink 94 assert response.status_code == 200 # Form re-rendered with errors
c588255… ragelink 95
c588255… ragelink 96 def test_create_denied_for_viewer(self, viewer_client):
c588255… ragelink 97 response = viewer_client.get(reverse("organization:user_create"))
c588255… ragelink 98 assert response.status_code == 403
c588255… ragelink 99
c588255… ragelink 100 def test_create_denied_for_anon(self, client):
c588255… ragelink 101 response = client.get(reverse("organization:user_create"))
c588255… ragelink 102 assert response.status_code == 302 # Redirect to login
c588255… ragelink 103
c588255… ragelink 104 def test_create_allowed_for_org_admin(self, org_admin_client, org):
c588255… ragelink 105 response = org_admin_client.post(
c588255… ragelink 106 reverse("organization:user_create"),
c588255… ragelink 107 {
c588255… ragelink 108 "username": "orgcreated",
c588255… ragelink 109 "email": "[email protected]",
c588255… ragelink 110 "password1": "Str0ng!Pass99",
c588255… ragelink 111 "password2": "Str0ng!Pass99",
c588255… ragelink 112 },
c588255… ragelink 113 )
c588255… ragelink 114 assert response.status_code == 302
c588255… ragelink 115 assert User.objects.filter(username="orgcreated").exists()
c588255… ragelink 116
c588255… ragelink 117
c588255… ragelink 118 # --- user_detail ---
c588255… ragelink 119
c588255… ragelink 120
c588255… ragelink 121 @pytest.mark.django_db
c588255… ragelink 122 class TestUserDetail:
c588255… ragelink 123 def test_view_user(self, admin_client, target_user):
c588255… ragelink 124 response = admin_client.get(reverse("organization:user_detail", kwargs={"username": "targetuser"}))
c588255… ragelink 125 assert response.status_code == 200
c588255… ragelink 126 content = response.content.decode()
c588255… ragelink 127 assert "targetuser" in content
c588255… ragelink 128 assert "[email protected]" in content
c588255… ragelink 129 assert "Target User" in content
c588255… ragelink 130
c588255… ragelink 131 def test_view_shows_teams(self, admin_client, target_user, sample_team):
c588255… ragelink 132 sample_team.members.add(target_user)
c588255… ragelink 133 response = admin_client.get(reverse("organization:user_detail", kwargs={"username": "targetuser"}))
c588255… ragelink 134 assert response.status_code == 200
c588255… ragelink 135 assert "Core Devs" in response.content.decode()
c588255… ragelink 136
c588255… ragelink 137 def test_view_denied_for_no_perm(self, no_perm_client, target_user):
c588255… ragelink 138 response = no_perm_client.get(reverse("organization:user_detail", kwargs={"username": "targetuser"}))
c588255… ragelink 139 assert response.status_code == 403
c588255… ragelink 140
c588255… ragelink 141 def test_view_allowed_for_viewer(self, viewer_client, target_user):
c588255… ragelink 142 response = viewer_client.get(reverse("organization:user_detail", kwargs={"username": "targetuser"}))
c588255… ragelink 143 assert response.status_code == 200
c588255… ragelink 144
c588255… ragelink 145 def test_view_404_for_missing_user(self, admin_client):
c588255… ragelink 146 response = admin_client.get(reverse("organization:user_detail", kwargs={"username": "nonexistent"}))
c588255… ragelink 147 assert response.status_code == 404
c588255… ragelink 148
c588255… ragelink 149
c588255… ragelink 150 # --- user_edit ---
c588255… ragelink 151
c588255… ragelink 152
c588255… ragelink 153 @pytest.mark.django_db
c588255… ragelink 154 class TestUserEdit:
c588255… ragelink 155 def test_get_edit_form(self, admin_client, target_user):
c588255… ragelink 156 response = admin_client.get(reverse("organization:user_edit", kwargs={"username": "targetuser"}))
c588255… ragelink 157 assert response.status_code == 200
c588255… ragelink 158 content = response.content.decode()
c588255… ragelink 159 assert "Edit targetuser" in content
c588255… ragelink 160
c588255… ragelink 161 def test_edit_user(self, admin_client, target_user):
c588255… ragelink 162 response = admin_client.post(
c588255… ragelink 163 reverse("organization:user_edit", kwargs={"username": "targetuser"}),
c588255… ragelink 164 {
c588255… ragelink 165 "email": "[email protected]",
c588255… ragelink 166 "first_name": "Updated",
c588255… ragelink 167 "last_name": "Name",
c588255… ragelink 168 "is_active": "on",
c588255… ragelink 169 },
c588255… ragelink 170 )
c588255… ragelink 171 assert response.status_code == 302
c588255… ragelink 172 target_user.refresh_from_db()
c588255… ragelink 173 assert target_user.email == "[email protected]"
c588255… ragelink 174 assert target_user.first_name == "Updated"
c588255… ragelink 175
c588255… ragelink 176 def test_edit_deactivate_user(self, admin_client, target_user):
c588255… ragelink 177 response = admin_client.post(
c588255… ragelink 178 reverse("organization:user_edit", kwargs={"username": "targetuser"}),
c588255… ragelink 179 {
c588255… ragelink 180 "email": "[email protected]",
c588255… ragelink 181 "first_name": "Target",
c588255… ragelink 182 "last_name": "User",
c588255… ragelink 183 # is_active omitted = False for checkbox
c588255… ragelink 184 },
c588255… ragelink 185 )
c588255… ragelink 186 assert response.status_code == 302
c588255… ragelink 187 target_user.refresh_from_db()
c588255… ragelink 188 assert target_user.is_active is False
c588255… ragelink 189
c588255… ragelink 190 def test_edit_self_cannot_deactivate(self, admin_client, admin_user):
c588255… ragelink 191 """Superuser editing themselves should not be able to toggle is_active."""
c588255… ragelink 192 response = admin_client.post(
c588255… ragelink 193 reverse("organization:user_edit", kwargs={"username": "admin"}),
c588255… ragelink 194 {
c588255… ragelink 195 "email": "[email protected]",
c588255… ragelink 196 "first_name": "Admin",
c588255… ragelink 197 "last_name": "",
c588255… ragelink 198 # is_active omitted -- but field is disabled so value comes from instance
c588255… ragelink 199 },
c588255… ragelink 200 )
c588255… ragelink 201 assert response.status_code == 302
c588255… ragelink 202 admin_user.refresh_from_db()
c588255… ragelink 203 # Should still be active because the field was disabled
c588255… ragelink 204 assert admin_user.is_active is True
c588255… ragelink 205
c588255… ragelink 206 def test_edit_denied_for_viewer(self, viewer_client, target_user):
c588255… ragelink 207 response = viewer_client.get(reverse("organization:user_edit", kwargs={"username": "targetuser"}))
c588255… ragelink 208 assert response.status_code == 403
c588255… ragelink 209
c588255… ragelink 210 def test_edit_denied_for_no_perm(self, no_perm_client, target_user):
c588255… ragelink 211 response = no_perm_client.get(reverse("organization:user_edit", kwargs={"username": "targetuser"}))
c588255… ragelink 212 assert response.status_code == 403
c588255… ragelink 213
c588255… ragelink 214 def test_edit_allowed_for_org_admin(self, org_admin_client, target_user):
c588255… ragelink 215 response = org_admin_client.post(
c588255… ragelink 216 reverse("organization:user_edit", kwargs={"username": "targetuser"}),
c588255… ragelink 217 {
c588255… ragelink 218 "email": "[email protected]",
c588255… ragelink 219 "first_name": "Org",
c588255… ragelink 220 "last_name": "Edited",
c588255… ragelink 221 "is_active": "on",
c588255… ragelink 222 },
c588255… ragelink 223 )
c588255… ragelink 224 assert response.status_code == 302
c588255… ragelink 225 target_user.refresh_from_db()
c588255… ragelink 226 assert target_user.email == "[email protected]"
c588255… ragelink 227
c588255… ragelink 228
c588255… ragelink 229 # --- user_password ---
c588255… ragelink 230
c588255… ragelink 231
c588255… ragelink 232 @pytest.mark.django_db
c588255… ragelink 233 class TestUserPassword:
c588255… ragelink 234 def test_get_password_form(self, admin_client, target_user):
c588255… ragelink 235 response = admin_client.get(reverse("organization:user_password", kwargs={"username": "targetuser"}))
c588255… ragelink 236 assert response.status_code == 200
c588255… ragelink 237 assert "Change Password" in response.content.decode()
c588255… ragelink 238
c588255… ragelink 239 def test_change_password(self, admin_client, target_user):
c588255… ragelink 240 response = admin_client.post(
c588255… ragelink 241 reverse("organization:user_password", kwargs={"username": "targetuser"}),
c588255… ragelink 242 {
c588255… ragelink 243 "new_password1": "NewStr0ng!Pass99",
c588255… ragelink 244 "new_password2": "NewStr0ng!Pass99",
c588255… ragelink 245 },
c588255… ragelink 246 )
c588255… ragelink 247 assert response.status_code == 302
c588255… ragelink 248 target_user.refresh_from_db()
c588255… ragelink 249 assert target_user.check_password("NewStr0ng!Pass99")
c588255… ragelink 250
c588255… ragelink 251 def test_change_password_mismatch(self, admin_client, target_user):
c588255… ragelink 252 response = admin_client.post(
c588255… ragelink 253 reverse("organization:user_password", kwargs={"username": "targetuser"}),
c588255… ragelink 254 {
c588255… ragelink 255 "new_password1": "NewStr0ng!Pass99",
c588255… ragelink 256 "new_password2": "different",
c588255… ragelink 257 },
c588255… ragelink 258 )
c588255… ragelink 259 assert response.status_code == 200 # Form re-rendered
c588255… ragelink 260 target_user.refresh_from_db()
c588255… ragelink 261 assert target_user.check_password("testpass123") # Unchanged
c588255… ragelink 262
c588255… ragelink 263 def test_change_own_password(self, target_user):
c588255… ragelink 264 """A regular user (no special perms) can change their own password."""
c588255… ragelink 265 c = Client()
c588255… ragelink 266 c.login(username="targetuser", password="testpass123")
c588255… ragelink 267 response = c.post(
c588255… ragelink 268 reverse("organization:user_password", kwargs={"username": "targetuser"}),
c588255… ragelink 269 {
c588255… ragelink 270 "new_password1": "MyNewStr0ng!Pass99",
c588255… ragelink 271 "new_password2": "MyNewStr0ng!Pass99",
c588255… ragelink 272 },
c588255… ragelink 273 )
c588255… ragelink 274 assert response.status_code == 302
c588255… ragelink 275 target_user.refresh_from_db()
c588255… ragelink 276 assert target_user.check_password("MyNewStr0ng!Pass99")
c588255… ragelink 277
c588255… ragelink 278 def test_change_other_password_denied_for_no_perm(self, no_perm_client, target_user):
c588255… ragelink 279 response = no_perm_client.post(
c588255… ragelink 280 reverse("organization:user_password", kwargs={"username": "targetuser"}),
c588255… ragelink 281 {
c588255… ragelink 282 "new_password1": "HackedStr0ng!Pass99",
c588255… ragelink 283 "new_password2": "HackedStr0ng!Pass99",
c588255… ragelink 284 },
c588255… ragelink 285 )
c588255… ragelink 286 assert response.status_code == 403
c588255… ragelink 287 target_user.refresh_from_db()
c588255… ragelink 288 assert target_user.check_password("testpass123") # Unchanged
c588255… ragelink 289
c588255… ragelink 290 def test_change_other_password_denied_for_viewer(self, viewer_client, target_user):
c588255… ragelink 291 response = viewer_client.post(
c588255… ragelink 292 reverse("organization:user_password", kwargs={"username": "targetuser"}),
c588255… ragelink 293 {
c588255… ragelink 294 "new_password1": "HackedStr0ng!Pass99",
c588255… ragelink 295 "new_password2": "HackedStr0ng!Pass99",
c588255… ragelink 296 },
c588255… ragelink 297 )
c588255… ragelink 298 assert response.status_code == 403
c588255… ragelink 299
c588255… ragelink 300 def test_change_password_denied_for_anon(self, client, target_user):
c588255… ragelink 301 response = client.get(reverse("organization:user_password", kwargs={"username": "targetuser"}))
c588255… ragelink 302 assert response.status_code == 302 # Redirect to login
c588255… ragelink 303
c588255… ragelink 304 def test_change_other_password_allowed_for_org_admin(self, org_admin_client, target_user):
c588255… ragelink 305 response = org_admin_client.post(
c588255… ragelink 306 reverse("organization:user_password", kwargs={"username": "targetuser"}),
c588255… ragelink 307 {
c588255… ragelink 308 "new_password1": "OrgAdminSet!Pass99",
c588255… ragelink 309 "new_password2": "OrgAdminSet!Pass99",
c588255… ragelink 310 },
c588255… ragelink 311 )
c588255… ragelink 312 assert response.status_code == 302
c588255… ragelink 313 target_user.refresh_from_db()
c588255… ragelink 314 assert target_user.check_password("OrgAdminSet!Pass99")
c588255… ragelink 315
c588255… ragelink 316
c588255… ragelink 317 # --- member_list updates ---
c588255… ragelink 318
c588255… ragelink 319
c588255… ragelink 320 @pytest.mark.django_db
c588255… ragelink 321 class TestMemberListUpdates:
c588255… ragelink 322 def test_usernames_are_clickable_links(self, admin_client, org, admin_user):
c588255… ragelink 323 response = admin_client.get(reverse("organization:members"))
c588255… ragelink 324 assert response.status_code == 200
c588255… ragelink 325 content = response.content.decode()
c588255… ragelink 326 expected_url = reverse("organization:user_detail", kwargs={"username": admin_user.username})
c588255… ragelink 327 assert expected_url in content
c588255… ragelink 328
c588255… ragelink 329 def test_create_user_button_visible_for_superuser(self, admin_client, org):
c588255… ragelink 330 response = admin_client.get(reverse("organization:members"))
c588255… ragelink 331 assert response.status_code == 200
c588255… ragelink 332 content = response.content.decode()
c588255… ragelink 333 assert "Create User" in content
c588255… ragelink 334
c588255… ragelink 335 def test_create_user_button_hidden_for_viewer(self, viewer_client, org):
c588255… ragelink 336 response = viewer_client.get(reverse("organization:members"))
c588255… ragelink 337 assert response.status_code == 200
c588255… ragelink 338 content = response.content.decode()
c588255… ragelink 339 assert "Create User" not in content

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button