feat(users): add transactional employee deletion and token revocation
All checks were successful
State Corp Backend CI/CD / Quality gate (push) Successful in 2m42s
State Corp Backend CI/CD / Build linux/amd64 images once (push) Successful in 5m29s
State Corp Backend CI/CD / Refresh and release internal main (push) Has been skipped
State Corp Backend CI/CD / Release customer main (push) Has been skipped
State Corp Backend CI/CD / Release dev (push) Successful in 36s

This commit is contained in:
Aleksandr Meshchryakov
2026-09-13 22:55:31 +02:00
parent ef087d5559
commit e99616ed6d
9 changed files with 971 additions and 69 deletions

View File

@@ -0,0 +1,122 @@
"""Django admin must share the account deletion and access guards."""
from unittest.mock import patch
from apps.core.exceptions import BadRequestError, ConflictError, PermissionDeniedError
from apps.user.admin import UserAdmin
from apps.user.management import ensure_admins_remain, lock_managed_users
from apps.user.models import User
from django.contrib.admin import AdminSite
from django.contrib.admin.models import LogEntry
from django.test import RequestFactory, TestCase
from django.urls import reverse
from rest_framework_simplejwt.token_blacklist.models import OutstandingToken
from rest_framework_simplejwt.tokens import RefreshToken
from tests.apps.user.factories import UserFactory
class ManagedUserAdminTest(TestCase):
def setUp(self):
self.actor = UserFactory.create_superuser()
self.user = UserFactory.create_user()
self.model_admin = UserAdmin(User, AdminSite())
self.request = RequestFactory().post("/admin/user/user/")
self.request.user = self.actor
self.initial_user_count = User.objects.count()
def test_admin_single_and_bulk_delete_clean_up_tokens(self):
second = UserFactory.create_user()
first_id, second_id = self.user.pk, second.pk
RefreshToken.for_user(self.user)
RefreshToken.for_user(second)
self.model_admin.delete_model(self.request, self.user)
self.model_admin.delete_queryset(
self.request, User.objects.filter(pk=second_id)
)
self.assertFalse(User.objects.filter(pk__in=[first_id, second_id]).exists())
self.assertFalse(
OutstandingToken.objects.filter(user_id__in=[first_id, second_id]).exists()
)
def test_bulk_delete_is_atomic_when_selection_includes_actor(self):
with self.assertRaises(BadRequestError):
self.model_admin.delete_queryset(self.request, User.objects.all())
self.assertEqual(User.objects.count(), self.initial_user_count)
def test_admin_bulk_deactivate_and_change_form_cannot_disable_self(self):
with self.assertRaises(BadRequestError):
self.model_admin.deactivate_users(self.request, User.objects.all())
self.assertTrue(User.objects.get(pk=self.user.pk).is_active)
edited_actor = User.objects.get(pk=self.actor.pk)
edited_actor.is_staff = False
with self.assertRaises(BadRequestError):
self.model_admin.save_model(self.request, edited_actor, None, True)
self.assertTrue(User.objects.get(pk=self.actor.pk).is_staff)
def test_admin_rechecks_actor_before_saving_or_bulk_deactivating(self):
User.objects.filter(pk=self.actor.pk).update(is_active=False)
with self.assertRaises(PermissionDeniedError):
self.model_admin.save_model(self.request, self.user, None, True)
with self.assertRaises(PermissionDeniedError):
self.model_admin.deactivate_users(
self.request, User.objects.filter(pk=self.user.pk)
)
def test_bulk_deactivation_preserves_other_administrator(self):
with patch.object(self.model_admin, "message_user"):
self.model_admin.deactivate_users(
self.request, User.objects.filter(pk=self.user.pk)
)
self.assertFalse(User.objects.get(pk=self.user.pk).is_active)
self.assertTrue(User.objects.get(pk=self.actor.pk).is_active)
def test_last_active_administrator_guard_excludes_inactive_staff(self):
User.objects.exclude(pk=self.actor.pk).update(is_active=False)
inactive = UserFactory.create_user(is_staff=True, is_active=False)
with lock_managed_users(
actor_id=self.actor.pk, user_ids=[inactive.pk]
), self.assertRaises(ConflictError) as error:
ensure_admins_remain(removed_ids=[self.actor.pk])
self.assertEqual(error.exception.code, "last_active_admin")
def test_admin_selected_delete_failure_does_not_publish_false_audit_entries(self):
self.client.force_login(self.actor)
response = self.client.post(
reverse("admin:user_user_changelist"),
{
"action": "delete_selected",
"post": "yes",
"_selected_action": [str(self.actor.pk), str(self.user.pk)],
},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(User.objects.count(), self.initial_user_count)
self.assertEqual(LogEntry.objects.count(), 0)
def test_admin_single_delete_error_is_presented_without_server_error(self):
self.client.force_login(self.actor)
response = self.client.post(
reverse("admin:user_user_delete", args=[self.actor.pk]), {"post": "yes"}
)
self.assertEqual(response.status_code, 302)
self.assertTrue(User.objects.filter(pk=self.actor.pk).exists())
self.assertEqual(LogEntry.objects.count(), 0)
def test_separate_admin_password_route_rechecks_actor(self):
old_hash = self.user.password
User.objects.filter(pk=self.actor.pk).update(is_staff=False)
request = RequestFactory().post(
"/admin/user/user/password/",
{
"password1": "changed-test-password",
"password2": "changed-test-password",
},
)
request.user = self.actor
with patch.object(self.model_admin, "message_user") as message:
response = self.model_admin.user_change_password(request, str(self.user.pk))
self.assertEqual(response.status_code, 302)
message.assert_called_once()
self.user.refresh_from_db()
self.assertEqual(self.user.password, old_hash)