FossilRepo

fossilrepo / tests / test_roles.py
Source Blame History 659 lines
c588255… ragelink 1 import pytest
c588255… ragelink 2 from django.contrib.auth.models import Group, Permission, 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, OrgRole
c588255… ragelink 7
c588255… ragelink 8
c588255… ragelink 9 @pytest.fixture
c588255… ragelink 10 def roles(db):
c588255… ragelink 11 """Seed default roles via management command."""
c588255… ragelink 12 from django.core.management import call_command
c588255… ragelink 13
c588255… ragelink 14 call_command("seed_roles")
c588255… ragelink 15 return OrgRole.objects.all()
c588255… ragelink 16
c588255… ragelink 17
c588255… ragelink 18 @pytest.fixture
c588255… ragelink 19 def admin_role(roles):
c588255… ragelink 20 return OrgRole.objects.get(slug="admin")
c588255… ragelink 21
c588255… ragelink 22
c588255… ragelink 23 @pytest.fixture
c588255… ragelink 24 def viewer_role(roles):
c588255… ragelink 25 return OrgRole.objects.get(slug="viewer")
c588255… ragelink 26
c588255… ragelink 27
c588255… ragelink 28 @pytest.fixture
c588255… ragelink 29 def developer_role(roles):
c588255… ragelink 30 return OrgRole.objects.get(slug="developer")
c588255… ragelink 31
c588255… ragelink 32
c588255… ragelink 33 @pytest.fixture
c588255… ragelink 34 def manager_role(roles):
c588255… ragelink 35 return OrgRole.objects.get(slug="manager")
c588255… ragelink 36
c588255… ragelink 37
c588255… ragelink 38 @pytest.fixture
c588255… ragelink 39 def target_user(db, org, admin_user):
c588255… ragelink 40 user = User.objects.create_user(
c588255… ragelink 41 username="targetuser", email="[email protected]", password="testpass123", first_name="Target", last_name="User"
c588255… ragelink 42 )
c588255… ragelink 43 OrganizationMember.objects.create(member=user, organization=org, created_by=admin_user)
c588255… ragelink 44 return user
c588255… ragelink 45
c588255… ragelink 46
c588255… ragelink 47 @pytest.fixture
c588255… ragelink 48 def org_admin_user(db, org):
c588255… ragelink 49 """Non-superuser with ORGANIZATION_CHANGE permission."""
c588255… ragelink 50 user = User.objects.create_user(username="orgadmin", email="[email protected]", password="testpass123")
c588255… ragelink 51 group, _ = Group.objects.get_or_create(name="OrgAdmins")
c588255… ragelink 52 change_perm = Permission.objects.get(content_type__app_label="organization", codename="change_organization")
c588255… ragelink 53 view_perm = Permission.objects.get(content_type__app_label="organization", codename="view_organization")
c588255… ragelink 54 view_member_perm = Permission.objects.get(content_type__app_label="organization", codename="view_organizationmember")
c588255… ragelink 55 group.permissions.add(change_perm, view_perm, view_member_perm)
c588255… ragelink 56 user.groups.add(group)
c588255… ragelink 57 OrganizationMember.objects.create(member=user, organization=org)
c588255… ragelink 58 return user
c588255… ragelink 59
c588255… ragelink 60
c588255… ragelink 61 @pytest.fixture
c588255… ragelink 62 def org_admin_client(org_admin_user):
c588255… ragelink 63 c = Client()
c588255… ragelink 64 c.login(username="orgadmin", password="testpass123")
c588255… ragelink 65 return c
c588255… ragelink 66
c588255… ragelink 67
c588255… ragelink 68 # --- OrgRole model ---
c588255… ragelink 69
c588255… ragelink 70
c588255… ragelink 71 @pytest.mark.django_db
c588255… ragelink 72 class TestOrgRoleModel:
c588255… ragelink 73 def test_seed_creates_four_roles(self, roles):
c588255… ragelink 74 assert OrgRole.objects.count() == 4
c588255… ragelink 75
c588255… ragelink 76 def test_seed_idempotent(self, roles):
c588255… ragelink 77 from django.core.management import call_command
c588255… ragelink 78
c588255… ragelink 79 call_command("seed_roles")
c588255… ragelink 80 assert OrgRole.objects.count() == 4
c588255… ragelink 81
c588255… ragelink 82 def test_admin_role_has_all_app_permissions(self, admin_role):
c588255… ragelink 83 app_perms = Permission.objects.filter(content_type__app_label__in=["organization", "projects", "pages", "fossil"]).count()
c588255… ragelink 84 assert admin_role.permissions.count() == app_perms
c588255… ragelink 85
c588255… ragelink 86 def test_viewer_role_is_default(self, viewer_role):
c588255… ragelink 87 assert viewer_role.is_default is True
c588255… ragelink 88
c588255… ragelink 89 def test_admin_role_not_default(self, admin_role):
c588255… ragelink 90 assert admin_role.is_default is False
c588255… ragelink 91
c588255… ragelink 92 def test_viewer_has_only_view_permissions(self, viewer_role):
c588255… ragelink 93 for perm in viewer_role.permissions.all():
c588255… ragelink 94 assert perm.codename.startswith("view_"), f"Viewer role should only have view_ permissions, got {perm.codename}"
c588255… ragelink 95
c588255… ragelink 96 def test_developer_has_add_page(self, developer_role):
c588255… ragelink 97 assert developer_role.permissions.filter(codename="add_page").exists()
c588255… ragelink 98
c588255… ragelink 99 def test_developer_no_delete_project(self, developer_role):
c588255… ragelink 100 assert not developer_role.permissions.filter(codename="delete_project").exists()
c588255… ragelink 101
c588255… ragelink 102 def test_manager_has_change_organization(self, manager_role):
c588255… ragelink 103 assert manager_role.permissions.filter(codename="change_organization").exists()
c588255… ragelink 104
c588255… ragelink 105
c588255… ragelink 106 # --- apply_to_user ---
c588255… ragelink 107
c588255… ragelink 108
c588255… ragelink 109 @pytest.mark.django_db
c588255… ragelink 110 class TestApplyToUser:
c588255… ragelink 111 def test_apply_creates_role_group(self, viewer_role, target_user):
c588255… ragelink 112 viewer_role.apply_to_user(target_user)
c588255… ragelink 113 assert target_user.groups.filter(name="role_viewer").exists()
c588255… ragelink 114
c588255… ragelink 115 def test_apply_sets_permissions(self, viewer_role, target_user):
c588255… ragelink 116 viewer_role.apply_to_user(target_user)
c588255… ragelink 117 assert target_user.has_perm("organization.view_organization")
c588255… ragelink 118
c588255… ragelink 119 def test_apply_replaces_old_role(self, viewer_role, admin_role, target_user):
c588255… ragelink 120 viewer_role.apply_to_user(target_user)
c588255… ragelink 121 admin_role.apply_to_user(target_user)
c588255… ragelink 122 # Should only be in admin role group now
c588255… ragelink 123 role_groups = target_user.groups.filter(name__startswith="role_")
c588255… ragelink 124 assert role_groups.count() == 1
c588255… ragelink 125 assert role_groups.first().name == "role_admin"
c588255… ragelink 126
c588255… ragelink 127 def test_remove_role_groups(self, viewer_role, target_user):
c588255… ragelink 128 viewer_role.apply_to_user(target_user)
c588255… ragelink 129 assert target_user.groups.filter(name__startswith="role_").count() == 1
c588255… ragelink 130 OrgRole.remove_role_groups(target_user)
c588255… ragelink 131 assert target_user.groups.filter(name__startswith="role_").count() == 0
c588255… ragelink 132
c588255… ragelink 133
c588255… ragelink 134 # --- role_list view ---
c588255… ragelink 135
c588255… ragelink 136
c588255… ragelink 137 @pytest.mark.django_db
c588255… ragelink 138 class TestRoleListView:
c588255… ragelink 139 def test_list_empty(self, admin_client, org):
c588255… ragelink 140 response = admin_client.get(reverse("organization:role_list"))
c588255… ragelink 141 assert response.status_code == 200
c588255… ragelink 142 assert "No roles defined" in response.content.decode()
c588255… ragelink 143
c588255… ragelink 144 def test_list_with_roles(self, admin_client, org, roles):
c588255… ragelink 145 response = admin_client.get(reverse("organization:role_list"))
c588255… ragelink 146 assert response.status_code == 200
c588255… ragelink 147 content = response.content.decode()
c588255… ragelink 148 assert "Admin" in content
c588255… ragelink 149 assert "Manager" in content
c588255… ragelink 150 assert "Developer" in content
c588255… ragelink 151 assert "Viewer" in content
c588255… ragelink 152
c588255… ragelink 153 def test_list_denied_for_no_perm(self, no_perm_client, org):
c588255… ragelink 154 response = no_perm_client.get(reverse("organization:role_list"))
c588255… ragelink 155 assert response.status_code == 403
c588255… ragelink 156
c588255… ragelink 157 def test_list_allowed_for_viewer(self, viewer_client, org, roles):
c588255… ragelink 158 response = viewer_client.get(reverse("organization:role_list"))
c588255… ragelink 159 assert response.status_code == 200
c588255… ragelink 160
c588255… ragelink 161 def test_list_denied_for_anon(self, client, org):
c588255… ragelink 162 response = client.get(reverse("organization:role_list"))
c588255… ragelink 163 assert response.status_code == 302 # redirect to login
c588255… ragelink 164
c588255… ragelink 165
c588255… ragelink 166 # --- role_detail view ---
c588255… ragelink 167
c588255… ragelink 168
c588255… ragelink 169 @pytest.mark.django_db
c588255… ragelink 170 class TestRoleDetailView:
c588255… ragelink 171 def test_detail_shows_role_info(self, admin_client, org, admin_role):
c588255… ragelink 172 response = admin_client.get(reverse("organization:role_detail", kwargs={"slug": "admin"}))
c588255… ragelink 173 assert response.status_code == 200
c588255… ragelink 174 content = response.content.decode()
c588255… ragelink 175 assert "Admin" in content
c588255… ragelink 176 assert "Full access" in content
c588255… ragelink 177
c588255… ragelink 178 def test_detail_shows_permissions(self, admin_client, org, viewer_role):
c588255… ragelink 179 response = admin_client.get(reverse("organization:role_detail", kwargs={"slug": "viewer"}))
c588255… ragelink 180 assert response.status_code == 200
c588255… ragelink 181 content = response.content.decode()
c588255… ragelink 182 assert "view_organization" in content
c588255… ragelink 183
c588255… ragelink 184 def test_detail_shows_members(self, admin_client, org, viewer_role, target_user):
c588255… ragelink 185 membership = OrganizationMember.objects.get(member=target_user, organization=org)
c588255… ragelink 186 membership.role = viewer_role
c588255… ragelink 187 membership.save()
c588255… ragelink 188 response = admin_client.get(reverse("organization:role_detail", kwargs={"slug": "viewer"}))
c588255… ragelink 189 assert response.status_code == 200
c588255… ragelink 190 assert "targetuser" in response.content.decode()
c588255… ragelink 191
c588255… ragelink 192 def test_detail_denied_for_no_perm(self, no_perm_client, org, viewer_role):
c588255… ragelink 193 response = no_perm_client.get(reverse("organization:role_detail", kwargs={"slug": "viewer"}))
c588255… ragelink 194 assert response.status_code == 403
c588255… ragelink 195
c588255… ragelink 196 def test_detail_404_for_missing_role(self, admin_client, org):
c588255… ragelink 197 response = admin_client.get(reverse("organization:role_detail", kwargs={"slug": "nonexistent"}))
c588255… ragelink 198 assert response.status_code == 404
c588255… ragelink 199
c588255… ragelink 200
c588255… ragelink 201 # --- role_initialize view ---
c588255… ragelink 202
c588255… ragelink 203
c588255… ragelink 204 @pytest.mark.django_db
c588255… ragelink 205 class TestRoleInitializeView:
c588255… ragelink 206 def test_initialize_creates_roles(self, admin_client, org):
c588255… ragelink 207 assert OrgRole.objects.count() == 0
c588255… ragelink 208 response = admin_client.post(reverse("organization:role_initialize"))
c588255… ragelink 209 assert response.status_code == 302
c588255… ragelink 210 assert OrgRole.objects.count() == 4
c588255… ragelink 211
c588255… ragelink 212 def test_initialize_denied_for_viewer(self, viewer_client, org):
c588255… ragelink 213 response = viewer_client.post(reverse("organization:role_initialize"))
c588255… ragelink 214 assert response.status_code == 403
c588255… ragelink 215
c588255… ragelink 216 def test_initialize_denied_for_no_perm(self, no_perm_client, org):
c588255… ragelink 217 response = no_perm_client.post(reverse("organization:role_initialize"))
c588255… ragelink 218 assert response.status_code == 403
c588255… ragelink 219
c588255… ragelink 220 def test_initialize_denied_for_anon(self, client, org):
c588255… ragelink 221 response = client.post(reverse("organization:role_initialize"))
c588255… ragelink 222 assert response.status_code == 302 # redirect to login
c588255… ragelink 223
c588255… ragelink 224 def test_initialize_allowed_for_org_admin(self, org_admin_client, org):
c588255… ragelink 225 response = org_admin_client.post(reverse("organization:role_initialize"))
c588255… ragelink 226 assert response.status_code == 302
c588255… ragelink 227 assert OrgRole.objects.count() == 4
c588255… ragelink 228
c588255… ragelink 229
c588255… ragelink 230 # --- user_create with role ---
c588255… ragelink 231
c588255… ragelink 232
c588255… ragelink 233 @pytest.mark.django_db
c588255… ragelink 234 class TestUserCreateWithRole:
c588255… ragelink 235 def test_create_user_with_role(self, admin_client, org, viewer_role):
c588255… ragelink 236 response = admin_client.post(
c588255… ragelink 237 reverse("organization:user_create"),
c588255… ragelink 238 {
c588255… ragelink 239 "username": "roleuser",
c588255… ragelink 240 "email": "[email protected]",
c588255… ragelink 241 "first_name": "Role",
c588255… ragelink 242 "last_name": "User",
c588255… ragelink 243 "password1": "Str0ng!Pass99",
c588255… ragelink 244 "password2": "Str0ng!Pass99",
c588255… ragelink 245 "role": viewer_role.pk,
c588255… ragelink 246 },
c588255… ragelink 247 )
c588255… ragelink 248 assert response.status_code == 302
c588255… ragelink 249 user = User.objects.get(username="roleuser")
c588255… ragelink 250 membership = OrganizationMember.objects.get(member=user, organization=org)
c588255… ragelink 251 assert membership.role == viewer_role
c588255… ragelink 252 # Verify role group was applied
c588255… ragelink 253 assert user.groups.filter(name="role_viewer").exists()
c588255… ragelink 254
c588255… ragelink 255 def test_create_user_without_role(self, admin_client, org, roles):
c588255… ragelink 256 response = admin_client.post(
c588255… ragelink 257 reverse("organization:user_create"),
c588255… ragelink 258 {
c588255… ragelink 259 "username": "noroleuser",
c588255… ragelink 260 "email": "[email protected]",
c588255… ragelink 261 "password1": "Str0ng!Pass99",
c588255… ragelink 262 "password2": "Str0ng!Pass99",
c588255… ragelink 263 },
c588255… ragelink 264 )
c588255… ragelink 265 assert response.status_code == 302
c588255… ragelink 266 user = User.objects.get(username="noroleuser")
c588255… ragelink 267 membership = OrganizationMember.objects.get(member=user, organization=org)
c588255… ragelink 268 assert membership.role is None
c588255… ragelink 269
c588255… ragelink 270 def test_create_form_has_role_field(self, admin_client, org, roles):
c588255… ragelink 271 response = admin_client.get(reverse("organization:user_create"))
c588255… ragelink 272 assert response.status_code == 200
c588255… ragelink 273 content = response.content.decode()
c588255… ragelink 274 assert "role" in content.lower()
c588255… ragelink 275
c588255… ragelink 276
c588255… ragelink 277 # --- user_edit with role ---
c588255… ragelink 278
c588255… ragelink 279
c588255… ragelink 280 @pytest.mark.django_db
c588255… ragelink 281 class TestUserEditWithRole:
c588255… ragelink 282 def test_edit_assigns_role(self, admin_client, org, target_user, viewer_role):
c588255… ragelink 283 response = admin_client.post(
c588255… ragelink 284 reverse("organization:user_edit", kwargs={"username": "targetuser"}),
c588255… ragelink 285 {
c588255… ragelink 286 "email": "[email protected]",
c588255… ragelink 287 "first_name": "Target",
c588255… ragelink 288 "last_name": "User",
c588255… ragelink 289 "is_active": "on",
c588255… ragelink 290 "role": viewer_role.pk,
c588255… ragelink 291 },
c588255… ragelink 292 )
c588255… ragelink 293 assert response.status_code == 302
c588255… ragelink 294 membership = OrganizationMember.objects.get(member=target_user, organization=org)
c588255… ragelink 295 assert membership.role == viewer_role
c588255… ragelink 296 assert target_user.groups.filter(name="role_viewer").exists()
c588255… ragelink 297
c588255… ragelink 298 def test_edit_changes_role(self, admin_client, org, target_user, viewer_role, admin_role):
c588255… ragelink 299 # First assign viewer role
c588255… ragelink 300 membership = OrganizationMember.objects.get(member=target_user, organization=org)
c588255… ragelink 301 membership.role = viewer_role
c588255… ragelink 302 membership.save()
c588255… ragelink 303 viewer_role.apply_to_user(target_user)
c588255… ragelink 304
c588255… ragelink 305 # Now change to admin role via edit
c588255… ragelink 306 response = admin_client.post(
c588255… ragelink 307 reverse("organization:user_edit", kwargs={"username": "targetuser"}),
c588255… ragelink 308 {
c588255… ragelink 309 "email": "[email protected]",
c588255… ragelink 310 "first_name": "Target",
c588255… ragelink 311 "last_name": "User",
c588255… ragelink 312 "is_active": "on",
c588255… ragelink 313 "role": admin_role.pk,
c588255… ragelink 314 },
c588255… ragelink 315 )
c588255… ragelink 316 assert response.status_code == 302
c588255… ragelink 317 membership.refresh_from_db()
c588255… ragelink 318 assert membership.role == admin_role
c588255… ragelink 319 # Old role group should be gone, new one should be present
c588255… ragelink 320 assert not target_user.groups.filter(name="role_viewer").exists()
c588255… ragelink 321 assert target_user.groups.filter(name="role_admin").exists()
c588255… ragelink 322
c588255… ragelink 323 def test_edit_removes_role(self, admin_client, org, target_user, viewer_role):
c588255… ragelink 324 membership = OrganizationMember.objects.get(member=target_user, organization=org)
c588255… ragelink 325 membership.role = viewer_role
c588255… ragelink 326 membership.save()
c588255… ragelink 327 viewer_role.apply_to_user(target_user)
c588255… ragelink 328
c588255… ragelink 329 # Submit without role
c588255… ragelink 330 response = admin_client.post(
c588255… ragelink 331 reverse("organization:user_edit", kwargs={"username": "targetuser"}),
c588255… ragelink 332 {
c588255… ragelink 333 "email": "[email protected]",
c588255… ragelink 334 "first_name": "Target",
c588255… ragelink 335 "last_name": "User",
c588255… ragelink 336 "is_active": "on",
c588255… ragelink 337 # role intentionally omitted
c588255… ragelink 338 },
c588255… ragelink 339 )
c588255… ragelink 340 assert response.status_code == 302
c588255… ragelink 341 membership.refresh_from_db()
c588255… ragelink 342 assert membership.role is None
c588255… ragelink 343 assert not target_user.groups.filter(name__startswith="role_").exists()
c588255… ragelink 344
c588255… ragelink 345 def test_edit_form_pre_selects_role(self, admin_client, org, target_user, viewer_role):
c588255… ragelink 346 membership = OrganizationMember.objects.get(member=target_user, organization=org)
c588255… ragelink 347 membership.role = viewer_role
c588255… ragelink 348 membership.save()
c588255… ragelink 349
c588255… ragelink 350 response = admin_client.get(reverse("organization:user_edit", kwargs={"username": "targetuser"}))
c588255… ragelink 351 assert response.status_code == 200
c588255… ragelink 352 content = response.content.decode()
c588255… ragelink 353 # The viewer option should be selected
c588255… ragelink 354 assert "selected" in content
c588255… ragelink 355
c588255… ragelink 356
c588255… ragelink 357 # --- member_list role column ---
c588255… ragelink 358
c588255… ragelink 359
c588255… ragelink 360 @pytest.mark.django_db
c588255… ragelink 361 class TestMemberListRoleColumn:
c588255… ragelink 362 def test_role_shown_in_member_list(self, admin_client, org, admin_user, viewer_role):
c588255… ragelink 363 membership = OrganizationMember.objects.get(member=admin_user, organization=org)
c588255… ragelink 364 membership.role = viewer_role
c588255… ragelink 365 membership.save()
c588255… ragelink 366
c588255… ragelink 367 response = admin_client.get(reverse("organization:members"))
c588255… ragelink 368 assert response.status_code == 200
c588255… ragelink 369 content = response.content.decode()
c588255… ragelink 370 assert "Role" in content # Column header
c588255… ragelink 371 assert "Viewer" in content # Role name
c588255… ragelink 372
c588255… ragelink 373 def test_no_role_shown_as_dash(self, admin_client, org, admin_user):
c588255… ragelink 374 response = admin_client.get(reverse("organization:members"))
c588255… ragelink 375 assert response.status_code == 200
c588255… ragelink 376 content = response.content.decode()
c588255… ragelink 377 assert "Role" in content # Column header
c588255… ragelink 378
c588255… ragelink 379
c588255… ragelink 380 # --- user_detail role display ---
c588255… ragelink 381
c588255… ragelink 382
c588255… ragelink 383 @pytest.mark.django_db
c588255… ragelink 384 class TestUserDetailRole:
c588255… ragelink 385 def test_detail_shows_role(self, admin_client, org, target_user, viewer_role):
c588255… ragelink 386 membership = OrganizationMember.objects.get(member=target_user, organization=org)
c588255… ragelink 387 membership.role = viewer_role
c588255… ragelink 388 membership.save()
c588255… ragelink 389
c588255… ragelink 390 response = admin_client.get(reverse("organization:user_detail", kwargs={"username": "targetuser"}))
c588255… ragelink 391 assert response.status_code == 200
c588255… ragelink 392 content = response.content.decode()
c588255… ragelink 393 assert "Viewer" in content
c588255… ragelink 394 assert "Read-only access" in content
c588255… ragelink 395
c588255… ragelink 396 def test_detail_shows_no_role_assigned(self, admin_client, org, target_user):
c588255… ragelink 397 response = admin_client.get(reverse("organization:user_detail", kwargs={"username": "targetuser"}))
c588255… ragelink 398 assert response.status_code == 200
c588255… ragelink 399 content = response.content.decode()
c588255… ragelink 400 assert "No role assigned" in content
c588255… ragelink 401
c588255… ragelink 402
c588255… ragelink 403 # --- role_create view ---
c588255… ragelink 404
c588255… ragelink 405
c588255… ragelink 406 @pytest.mark.django_db
c588255… ragelink 407 class TestRoleCreateView:
c588255… ragelink 408 def test_create_get_shows_form(self, admin_client, org):
c588255… ragelink 409 response = admin_client.get(reverse("organization:role_create"))
c588255… ragelink 410 assert response.status_code == 200
c588255… ragelink 411 content = response.content.decode()
c588255… ragelink 412 assert "New Role" in content
c588255… ragelink 413 assert "Permissions" in content
c588255… ragelink 414
c588255… ragelink 415 def test_create_saves_role(self, admin_client, org):
c588255… ragelink 416 perm = Permission.objects.filter(content_type__app_label="organization", codename="view_organization").first()
c588255… ragelink 417 response = admin_client.post(
c588255… ragelink 418 reverse("organization:role_create"),
c588255… ragelink 419 {"name": "Custom Role", "description": "A custom role", "permissions": [perm.pk]},
c588255… ragelink 420 )
c588255… ragelink 421 assert response.status_code == 302
c588255… ragelink 422 role = OrgRole.objects.get(slug="custom-role")
c588255… ragelink 423 assert role.name == "Custom Role"
c588255… ragelink 424 assert role.description == "A custom role"
c588255… ragelink 425 assert perm in role.permissions.all()
c588255… ragelink 426
c588255… ragelink 427 def test_create_without_permissions(self, admin_client, org):
c588255… ragelink 428 response = admin_client.post(
c588255… ragelink 429 reverse("organization:role_create"),
c588255… ragelink 430 {"name": "Empty Role", "description": "No permissions"},
c588255… ragelink 431 )
c588255… ragelink 432 assert response.status_code == 302
c588255… ragelink 433 role = OrgRole.objects.get(slug="empty-role")
c588255… ragelink 434 assert role.permissions.count() == 0
c588255… ragelink 435
c588255… ragelink 436 def test_create_with_is_default(self, admin_client, org):
c588255… ragelink 437 response = admin_client.post(
c588255… ragelink 438 reverse("organization:role_create"),
c588255… ragelink 439 {"name": "Default Custom", "description": "Default", "is_default": "on"},
c588255… ragelink 440 )
c588255… ragelink 441 assert response.status_code == 302
c588255… ragelink 442 role = OrgRole.objects.get(slug="default-custom")
c588255… ragelink 443 assert role.is_default is True
c588255… ragelink 444
c588255… ragelink 445 def test_create_denied_for_viewer(self, viewer_client, org):
c588255… ragelink 446 response = viewer_client.get(reverse("organization:role_create"))
c588255… ragelink 447 assert response.status_code == 403
c588255… ragelink 448
c588255… ragelink 449 def test_create_denied_for_no_perm(self, no_perm_client, org):
c588255… ragelink 450 response = no_perm_client.get(reverse("organization:role_create"))
c588255… ragelink 451 assert response.status_code == 403
c588255… ragelink 452
c588255… ragelink 453 def test_create_denied_for_anon(self, client, org):
c588255… ragelink 454 response = client.get(reverse("organization:role_create"))
c588255… ragelink 455 assert response.status_code == 302 # redirect to login
c588255… ragelink 456
c588255… ragelink 457 def test_create_allowed_for_org_admin(self, org_admin_client, org):
c588255… ragelink 458 response = org_admin_client.post(
c588255… ragelink 459 reverse("organization:role_create"),
c588255… ragelink 460 {"name": "OrgAdmin Role", "description": "Created by org admin"},
c588255… ragelink 461 )
c588255… ragelink 462 assert response.status_code == 302
c588255… ragelink 463 assert OrgRole.objects.filter(slug="orgadmin-role").exists()
c588255… ragelink 464
c588255… ragelink 465 def test_create_sets_created_by(self, admin_client, org, admin_user):
c588255… ragelink 466 response = admin_client.post(
c588255… ragelink 467 reverse("organization:role_create"),
c588255… ragelink 468 {"name": "Tracked Role", "description": "test"},
c588255… ragelink 469 )
c588255… ragelink 470 assert response.status_code == 302
c588255… ragelink 471 role = OrgRole.objects.get(slug="tracked-role")
c588255… ragelink 472 assert role.created_by == admin_user
c588255… ragelink 473
c588255… ragelink 474 def test_create_invalid_missing_name(self, admin_client, org):
c588255… ragelink 475 response = admin_client.post(
c588255… ragelink 476 reverse("organization:role_create"),
c588255… ragelink 477 {"description": "Missing name"},
c588255… ragelink 478 )
c588255… ragelink 479 assert response.status_code == 200 # re-renders form
c588255… ragelink 480 assert OrgRole.objects.count() == 0
c588255… ragelink 481
c588255… ragelink 482
c588255… ragelink 483 # --- role_edit view ---
c588255… ragelink 484
c588255… ragelink 485
c588255… ragelink 486 @pytest.mark.django_db
c588255… ragelink 487 class TestRoleEditView:
c588255… ragelink 488 def test_edit_get_shows_form(self, admin_client, org, viewer_role):
c588255… ragelink 489 response = admin_client.get(reverse("organization:role_edit", kwargs={"slug": "viewer"}))
c588255… ragelink 490 assert response.status_code == 200
c588255… ragelink 491 content = response.content.decode()
c588255… ragelink 492 assert "Edit Viewer" in content
c588255… ragelink 493 assert "Permissions" in content
c588255… ragelink 494
c588255… ragelink 495 def test_edit_updates_role(self, admin_client, org, viewer_role):
c588255… ragelink 496 response = admin_client.post(
c588255… ragelink 497 reverse("organization:role_edit", kwargs={"slug": "viewer"}),
c588255… ragelink 498 {"name": "Viewer Updated", "description": "Updated description"},
c588255… ragelink 499 )
c588255… ragelink 500 assert response.status_code == 302
c588255… ragelink 501 viewer_role.refresh_from_db()
c588255… ragelink 502 assert viewer_role.name == "Viewer Updated"
c588255… ragelink 503 assert viewer_role.description == "Updated description"
c588255… ragelink 504
c588255… ragelink 505 def test_edit_updates_permissions(self, admin_client, org, viewer_role):
c588255… ragelink 506 add_perm = Permission.objects.get(content_type__app_label="organization", codename="add_organization")
c588255… ragelink 507 response = admin_client.post(
c588255… ragelink 508 reverse("organization:role_edit", kwargs={"slug": "viewer"}),
c588255… ragelink 509 {"name": "Viewer", "description": "Updated", "permissions": [add_perm.pk]},
c588255… ragelink 510 )
c588255… ragelink 511 assert response.status_code == 302
c588255… ragelink 512 viewer_role.refresh_from_db()
c588255… ragelink 513 assert list(viewer_role.permissions.values_list("pk", flat=True)) == [add_perm.pk]
c588255… ragelink 514
c588255… ragelink 515 def test_edit_clears_permissions(self, admin_client, org, viewer_role):
c588255… ragelink 516 assert viewer_role.permissions.count() > 0
c588255… ragelink 517 response = admin_client.post(
c588255… ragelink 518 reverse("organization:role_edit", kwargs={"slug": "viewer"}),
c588255… ragelink 519 {"name": "Viewer", "description": "No perms now"},
c588255… ragelink 520 )
c588255… ragelink 521 assert response.status_code == 302
c588255… ragelink 522 viewer_role.refresh_from_db()
c588255… ragelink 523 assert viewer_role.permissions.count() == 0
c588255… ragelink 524
c588255… ragelink 525 def test_edit_pre_populates_permissions(self, admin_client, org, viewer_role):
c588255… ragelink 526 response = admin_client.get(reverse("organization:role_edit", kwargs={"slug": "viewer"}))
c588255… ragelink 527 assert response.status_code == 200
c588255… ragelink 528 content = response.content.decode()
c588255… ragelink 529 assert "checked" in content
c588255… ragelink 530
c588255… ragelink 531 def test_edit_sets_updated_by(self, admin_client, org, viewer_role, admin_user):
c588255… ragelink 532 response = admin_client.post(
c588255… ragelink 533 reverse("organization:role_edit", kwargs={"slug": "viewer"}),
c588255… ragelink 534 {"name": "Viewer", "description": "Audit check"},
c588255… ragelink 535 )
c588255… ragelink 536 assert response.status_code == 302
c588255… ragelink 537 viewer_role.refresh_from_db()
c588255… ragelink 538 assert viewer_role.updated_by == admin_user
c588255… ragelink 539
c588255… ragelink 540 def test_edit_denied_for_viewer(self, viewer_client, org, viewer_role):
c588255… ragelink 541 response = viewer_client.get(reverse("organization:role_edit", kwargs={"slug": "viewer"}))
c588255… ragelink 542 assert response.status_code == 403
c588255… ragelink 543
c588255… ragelink 544 def test_edit_denied_for_no_perm(self, no_perm_client, org, viewer_role):
c588255… ragelink 545 response = no_perm_client.get(reverse("organization:role_edit", kwargs={"slug": "viewer"}))
c588255… ragelink 546 assert response.status_code == 403
c588255… ragelink 547
c588255… ragelink 548 def test_edit_denied_for_anon(self, client, org, viewer_role):
c588255… ragelink 549 response = client.get(reverse("organization:role_edit", kwargs={"slug": "viewer"}))
c588255… ragelink 550 assert response.status_code == 302 # redirect to login
c588255… ragelink 551
c588255… ragelink 552 def test_edit_404_for_deleted_role(self, admin_client, org, viewer_role, admin_user):
c588255… ragelink 553 viewer_role.soft_delete(user=admin_user)
c588255… ragelink 554 response = admin_client.get(reverse("organization:role_edit", kwargs={"slug": "viewer"}))
c588255… ragelink 555 assert response.status_code == 404
c588255… ragelink 556
c588255… ragelink 557 def test_edit_404_for_missing_role(self, admin_client, org):
c588255… ragelink 558 response = admin_client.get(reverse("organization:role_edit", kwargs={"slug": "nonexistent"}))
c588255… ragelink 559 assert response.status_code == 404
c588255… ragelink 560
c588255… ragelink 561
c588255… ragelink 562 # --- role_delete view ---
c588255… ragelink 563
c588255… ragelink 564
c588255… ragelink 565 @pytest.mark.django_db
c588255… ragelink 566 class TestRoleDeleteView:
c588255… ragelink 567 def test_delete_get_shows_confirmation(self, admin_client, org, viewer_role):
c588255… ragelink 568 response = admin_client.get(reverse("organization:role_delete", kwargs={"slug": "viewer"}))
c588255… ragelink 569 assert response.status_code == 200
c588255… ragelink 570 content = response.content.decode()
c588255… ragelink 571 assert "Delete Role" in content
c588255… ragelink 572 assert "Viewer" in content
c588255… ragelink 573
c588255… ragelink 574 def test_delete_soft_deletes_role(self, admin_client, org, viewer_role):
c588255… ragelink 575 response = admin_client.post(reverse("organization:role_delete", kwargs={"slug": "viewer"}))
c588255… ragelink 576 assert response.status_code == 302
c588255… ragelink 577 viewer_role.refresh_from_db()
c588255… ragelink 578 assert viewer_role.deleted_at is not None
c588255… ragelink 579
c588255… ragelink 580 def test_delete_blocked_when_members_assigned(self, admin_client, org, viewer_role, target_user):
c588255… ragelink 581 membership = OrganizationMember.objects.get(member=target_user, organization=org)
c588255… ragelink 582 membership.role = viewer_role
c588255… ragelink 583 membership.save()
c588255… ragelink 584
c588255… ragelink 585 response = admin_client.post(reverse("organization:role_delete", kwargs={"slug": "viewer"}))
c588255… ragelink 586 assert response.status_code == 302 # redirects back to detail
c588255… ragelink 587 viewer_role.refresh_from_db()
c588255… ragelink 588 assert viewer_role.deleted_at is None # not deleted
c588255… ragelink 589
c588255… ragelink 590 def test_delete_shows_warning_for_members(self, admin_client, org, viewer_role, target_user):
c588255… ragelink 591 membership = OrganizationMember.objects.get(member=target_user, organization=org)
c588255… ragelink 592 membership.role = viewer_role
c588255… ragelink 593 membership.save()
c588255… ragelink 594
c588255… ragelink 595 response = admin_client.get(reverse("organization:role_delete", kwargs={"slug": "viewer"}))
c588255… ragelink 596 assert response.status_code == 200
c588255… ragelink 597 content = response.content.decode()
c588255… ragelink 598 assert "active member" in content
c588255… ragelink 599 assert "targetuser" in content
c588255… ragelink 600
c588255… ragelink 601 def test_delete_denied_for_viewer(self, viewer_client, org, viewer_role):
c588255… ragelink 602 response = viewer_client.get(reverse("organization:role_delete", kwargs={"slug": "viewer"}))
c588255… ragelink 603 assert response.status_code == 403
c588255… ragelink 604
c588255… ragelink 605 def test_delete_denied_for_no_perm(self, no_perm_client, org, viewer_role):
c588255… ragelink 606 response = no_perm_client.get(reverse("organization:role_delete", kwargs={"slug": "viewer"}))
c588255… ragelink 607 assert response.status_code == 403
c588255… ragelink 608
c588255… ragelink 609 def test_delete_denied_for_anon(self, client, org, viewer_role):
c588255… ragelink 610 response = client.get(reverse("organization:role_delete", kwargs={"slug": "viewer"}))
c588255… ragelink 611 assert response.status_code == 302 # redirect to login
c588255… ragelink 612
c588255… ragelink 613 def test_delete_404_for_deleted_role(self, admin_client, org, viewer_role, admin_user):
c588255… ragelink 614 viewer_role.soft_delete(user=admin_user)
c588255… ragelink 615 response = admin_client.get(reverse("organization:role_delete", kwargs={"slug": "viewer"}))
c588255… ragelink 616 assert response.status_code == 404
c588255… ragelink 617
c588255… ragelink 618 def test_delete_htmx_returns_redirect_header(self, admin_client, org, developer_role):
c588255… ragelink 619 response = admin_client.post(
c588255… ragelink 620 reverse("organization:role_delete", kwargs={"slug": "developer"}),
c588255… ragelink 621 HTTP_HX_REQUEST="true",
c588255… ragelink 622 )
c588255… ragelink 623 assert response.status_code == 200
c588255… ragelink 624 assert response.headers.get("HX-Redirect") == "/settings/roles/"
c588255… ragelink 625
c588255… ragelink 626
c588255… ragelink 627 # --- role_list Create Role button ---
c588255… ragelink 628
c588255… ragelink 629
c588255… ragelink 630 @pytest.mark.django_db
c588255… ragelink 631 class TestRoleListCreateButton:
c588255… ragelink 632 def test_create_button_shown_for_admin(self, admin_client, org, roles):
c588255… ragelink 633 response = admin_client.get(reverse("organization:role_list"))
c588255… ragelink 634 assert response.status_code == 200
c588255… ragelink 635 assert "Create Role" in response.content.decode()
c588255… ragelink 636
c588255… ragelink 637 def test_create_button_hidden_for_viewer(self, viewer_client, org, roles):
c588255… ragelink 638 response = viewer_client.get(reverse("organization:role_list"))
c588255… ragelink 639 assert response.status_code == 200
c588255… ragelink 640 assert "Create Role" not in response.content.decode()
c588255… ragelink 641
c588255… ragelink 642
c588255… ragelink 643 # --- role_detail Edit/Delete buttons ---
c588255… ragelink 644
c588255… ragelink 645
c588255… ragelink 646 @pytest.mark.django_db
c588255… ragelink 647 class TestRoleDetailButtons:
c588255… ragelink 648 def test_edit_button_shown_for_admin(self, admin_client, org, viewer_role):
c588255… ragelink 649 response = admin_client.get(reverse("organization:role_detail", kwargs={"slug": "viewer"}))
c588255… ragelink 650 assert response.status_code == 200
c588255… ragelink 651 content = response.content.decode()
c588255… ragelink 652 assert "Edit" in content
c588255… ragelink 653 assert "Delete" in content
c588255… ragelink 654
c588255… ragelink 655 def test_edit_button_hidden_for_viewer(self, viewer_client, org, viewer_role):
c588255… ragelink 656 response = viewer_client.get(reverse("organization:role_detail", kwargs={"slug": "viewer"}))
c588255… ragelink 657 assert response.status_code == 200
c588255… ragelink 658 content = response.content.decode()
c588255… ragelink 659 assert "Edit" not in content or "role_edit" 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