Files
state-corp-backend/tests/apps/user/test_management_concurrency.py
Aleksandr Meshchryakov e99616ed6d
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
feat(users): add transactional employee deletion and token revocation
2026-09-13 22:55:31 +02:00

160 lines
7.0 KiB
Python

"""Use real row locks to exercise concurrent administrative mutations."""
from concurrent.futures import ThreadPoolExecutor
from threading import Event
from apps.core.exceptions import PermissionDeniedError
from apps.user.admin import UserAdmin
from apps.user.models import User
from apps.user.services import UserService
from django.contrib.admin import AdminSite
from django.db import close_old_connections, connection, transaction
from django.test import RequestFactory, TransactionTestCase, skipUnlessDBFeature
from django.urls import reverse
from rest_framework.test import APIClient
from tests.apps.user.factories import UserFactory
@skipUnlessDBFeature("has_select_for_update")
class UserManagementConcurrencyTest(TransactionTestCase):
"""A request authenticated before deletion must lose mutation privileges."""
def _assert_deleted_actor_cannot_mutate(self, operation: str) -> None:
actor = UserFactory.create_superuser()
removed_actor = UserFactory.create_superuser()
actor_id, removed_id = actor.pk, removed_actor.pk
first_deleted = Event()
second_lock_query = Event()
release_first = Event()
def delete_other_actor():
close_old_connections()
try:
with transaction.atomic():
UserService.delete_user(removed_id, actor_id=actor_id)
first_deleted.set()
if not release_first.wait(10):
raise AssertionError("Concurrent test did not release deletion")
finally:
connection.close()
def observe_lock(execute, sql, params, many, context):
if "FOR UPDATE" in sql:
second_lock_query.set()
return execute(sql, params, many, context)
def stale_request():
close_old_connections()
try:
with connection.execute_wrapper(observe_lock):
if operation == "admin-deactivate":
request = RequestFactory().post("/admin/user/user/")
request.user = removed_actor
model_admin = UserAdmin(User, AdminSite())
try:
model_admin.deactivate_users(
request, User.objects.filter(pk=actor_id)
)
except PermissionDeniedError:
return 403
return 200
client = APIClient()
client.force_authenticate(removed_actor)
if operation == "self-password":
return client.post(
reverse("api_v1:user:password_change"),
{
"old_password": "testpass123",
"new_password": "changed-test-password",
"new_password_confirm": "changed-test-password",
},
format="json",
).status_code
if operation == "self-update":
return client.patch(
reverse("api_v1:user:user_update"),
{
"username": "replacement-admin",
},
format="json",
).status_code
if operation == "create-admin":
return client.post(
reverse("api_v1:user:admin_users"),
{
"username": "replacement-admin",
"email": "replacement@example.test",
"password": "integration-test-password",
"role": "admin",
"first_name": "Test",
"last_name": "Admin",
},
format="json",
).status_code
url = reverse("api_v1:user:admin-user-detail", args=[actor_id])
if operation == "delete":
return client.delete(url).status_code
if operation == "demote":
return client.patch(
url, {"role": "user"}, format="json"
).status_code
if operation == "patch-inactive":
return client.patch(
url, {"is_active": False}, format="json"
).status_code
url = reverse("api_v1:user:admin-user-deactivate", args=[actor_id])
return client.post(url).status_code
finally:
connection.close()
with ThreadPoolExecutor(max_workers=2) as pool:
first = pool.submit(delete_other_actor)
try:
self.assertTrue(
first_deleted.wait(10),
"First deletion did not reach its transaction barrier",
)
second = pool.submit(stale_request)
self.assertTrue(
second_lock_query.wait(10),
"Second request did not try to acquire row locks",
)
self.assertFalse(second.done())
finally:
release_first.set()
first.result(timeout=10)
expected_status = (
404 if operation in {"self-password", "self-update"} else 403
)
self.assertEqual(second.result(timeout=10), expected_status)
self.assertFalse(User.objects.filter(pk=removed_id).exists())
self.assertTrue(
User.objects.filter(pk=actor_id, is_active=True, is_staff=True).exists()
)
self.assertFalse(User.objects.filter(username="replacement-admin").exists())
def test_two_admins_cannot_delete_each_other(self):
self._assert_deleted_actor_cannot_mutate("delete")
def test_deleted_admin_cannot_deactivate_survivor(self):
self._assert_deleted_actor_cannot_mutate("deactivate")
def test_deleted_admin_cannot_demote_survivor(self):
self._assert_deleted_actor_cannot_mutate("demote")
def test_deleted_admin_cannot_patch_survivor_inactive(self):
self._assert_deleted_actor_cannot_mutate("patch-inactive")
def test_deleted_admin_cannot_use_django_admin_bulk_deactivation(self):
self._assert_deleted_actor_cannot_mutate("admin-deactivate")
def test_deleted_admin_cannot_create_a_replacement_admin(self):
self._assert_deleted_actor_cannot_mutate("create-admin")
def test_in_flight_self_password_change_cannot_recreate_deleted_user(self):
self._assert_deleted_actor_cannot_mutate("self-password")
def test_in_flight_self_update_cannot_recreate_deleted_user(self):
self._assert_deleted_actor_cannot_mutate("self-update")