""" Тесты прав доступа для API оборудования. Сценарии: 1. Создание оборудования: - Админ/staff → может выбирать любой отдел и ответственного - Модельные права (add_equipment) → может выбирать любой отдел и ответственного - Начальник отдела → только свой отдел, ответственный по умолчанию он сам (может выбрать из отдела) - Уполномоченный (скоуп-право) → только свой отдел, ответственный = начальник (без выбора) - Обычный сотрудник → нет доступа 2. Изменение/удаление: - Админ/staff → любое оборудование - Модельные права → любое оборудование - Начальник отдела → только своего отдела - Уполномоченный → только своего отдела - Ответственный → только изменение (не удаление) 3. Валидация: - Ответственный ДОЛЖЕН состоять в отделе оборудования """ from datetime import date from decimal import Decimal import pytest from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient from employees.constants import DeptPerm from employees.models import ( Department, DepartmentPermission, DepartmentRole, EmployeeDepartment, ) from procurement.models import Equipment, EquipmentCategory User = get_user_model() # Код права для управления оборудованием в отделе MANAGE_EQUIPMENT_CODE = DeptPerm.MANAGE_EQUIPMENT # ============================================================================= # Helpers # ============================================================================= _phone_counter = 0 def unique_phone() -> str: """Генерирует уникальный телефон.""" global _phone_counter _phone_counter += 1 return f"+7999{_phone_counter:07d}" def make_user( email: str, staff: bool = False, superuser: bool = False, verified: bool = True, ) -> User: """Создаёт пользователя.""" return User.objects.create_user( email=email, password="testpass123", phone_number=unique_phone(), first_name="Test", last_name="User", is_staff=staff, is_superuser=superuser, is_active=True, email_verified=verified, send_activation_email=False, ) def make_department(name: str, head: User = None) -> Department: """Создаёт отдел.""" return Department.objects.create(name=name, head=head) def ensure_dept_perm(code: str, name: str = None) -> DepartmentPermission: """Получает или создаёт скоуп-право отдела.""" perm, _ = DepartmentPermission.objects.get_or_create( code=code, defaults={"name": name or code} ) return perm def make_role(dept: Department, name: str, codes: list[str] = None) -> DepartmentRole: """Создаёт роль в отделе с указанными скоуп-правами.""" role = DepartmentRole.objects.create(department=dept, name=name) if codes: perms = [ensure_dept_perm(c) for c in codes] role.scoped_permissions.add(*perms) return role def add_user_to_dept( user: User, dept: Department, role: DepartmentRole = None, is_active: bool = True ) -> EmployeeDepartment: """Добавляет пользователя в отдел с ролью.""" link, _ = EmployeeDepartment.objects.update_or_create( employee=user, department=dept, defaults={"is_active": is_active, "role": role} ) return link def grant_model_permission(user: User, perm_codename: str, model_class) -> None: """Выдаёт модельное право пользователю.""" ct = ContentType.objects.get_for_model(model_class) # Используем get() чтобы получить существующий permission # Permissions создаются Django при миграциях perm = Permission.objects.get(content_type=ct, codename=perm_codename) user.user_permissions.add(perm) # Очищаем кэш прав пользователя if hasattr(user, "_perm_cache"): del user._perm_cache if hasattr(user, "_user_perm_cache"): del user._user_perm_cache def make_category(name: str = "Компьютеры") -> EquipmentCategory: """Создаёт категорию оборудования.""" cat, _ = EquipmentCategory.objects.get_or_create( name=name, defaults={"icon": "bi-laptop"} ) return cat def make_equipment( name: str, dept: Department, category: EquipmentCategory, responsible: User = None, inventory_number: str = None, ) -> Equipment: """Создаёт оборудование.""" if not inventory_number: count = Equipment.objects.count() inventory_number = f"INV-2025-{count + 1:04d}" return Equipment.objects.create( name=name, inventory_number=inventory_number, category=category, department=dept, responsible_person=responsible, purchase_date=date.today(), purchase_cost=Decimal("10000.00"), ) def equipment_create_url() -> str: """URL для создания оборудования.""" return reverse("api:v1:procurement:equipment-list") def equipment_detail_url(pk: int) -> str: """URL для конкретного оборудования.""" return reverse("api:v1:procurement:equipment-detail", args=[pk]) def valid_equipment_data( category: EquipmentCategory, dept: Department, responsible: User = None, name: str = "Ноутбук Dell", ) -> dict: """Валидные данные для создания оборудования.""" data = { "name": name, "category": category.id, "department": dept.id, "purchase_date": date.today().isoformat(), "purchase_cost": "50000.00", } if responsible: data["responsible_person"] = responsible.id return data # ============================================================================= # Fixtures # ============================================================================= @pytest.fixture def api_client() -> APIClient: return APIClient() @pytest.fixture def category(db) -> EquipmentCategory: return make_category() @pytest.fixture def admin_user(db) -> User: """Администратор (staff).""" return make_user("admin@example.com", staff=True) @pytest.fixture def superuser(db) -> User: """Суперпользователь.""" return make_user("super@example.com", superuser=True) @pytest.fixture def model_perm_user(db) -> User: """Пользователь с модельными правами на оборудование.""" user = make_user("modelperm@example.com") grant_model_permission(user, "add_equipment", Equipment) grant_model_permission(user, "change_equipment", Equipment) grant_model_permission(user, "delete_equipment", Equipment) # Перезагружаем из БД для обновления кэша прав user = User.objects.get(pk=user.pk) return user @pytest.fixture def dept_it(db) -> Department: """IT отдел (без начальника изначально).""" return make_department("IT отдел") @pytest.fixture def dept_hr(db) -> Department: """HR отдел.""" return make_department("HR отдел") @pytest.fixture def dept_head(db, dept_it) -> User: """Начальник IT отдела.""" user = make_user("head@example.com") dept_it.head = user dept_it.save() add_user_to_dept(user, dept_it) return user @pytest.fixture def dept_member(db, dept_it) -> User: """Обычный сотрудник IT отдела (без особых прав).""" user = make_user("member@example.com") add_user_to_dept(user, dept_it) return user @pytest.fixture def authorized_member(db, dept_it) -> User: """Уполномоченный сотрудник IT отдела (со скоуп-правом).""" user = make_user("authorized@example.com") role = make_role(dept_it, "Ответственный за оборудование", [MANAGE_EQUIPMENT_CODE]) add_user_to_dept(user, dept_it, role=role) return user @pytest.fixture def other_dept_head(db, dept_hr) -> User: """Начальник HR отдела.""" user = make_user("hr_head@example.com") dept_hr.head = user dept_hr.save() add_user_to_dept(user, dept_hr) return user @pytest.fixture def random_user(db) -> User: """Случайный пользователь без отдела.""" return make_user("random@example.com") # ============================================================================= # Tests: Создание оборудования - кто может # ============================================================================= @pytest.mark.django_db class TestEquipmentCreateAccess: """Тесты: кто может создавать оборудование.""" def test_unauthenticated_cannot_create(self, api_client, category, dept_it): """Неаутентифицированный пользователь не может создавать.""" data = valid_equipment_data(category, dept_it) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_401_UNAUTHORIZED def test_admin_can_create(self, api_client, admin_user, category, dept_it): """Админ может создавать оборудование.""" api_client.force_authenticate(user=admin_user) data = valid_equipment_data(category, dept_it) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_201_CREATED def test_superuser_can_create(self, api_client, superuser, category, dept_it): """Суперпользователь может создавать оборудование.""" api_client.force_authenticate(user=superuser) data = valid_equipment_data(category, dept_it) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_201_CREATED def test_model_perm_user_can_create( self, api_client, model_perm_user, category, dept_it ): """Пользователь с модельными правами может создавать.""" api_client.force_authenticate(user=model_perm_user) data = valid_equipment_data(category, dept_it) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_201_CREATED def test_dept_head_can_create_in_own_dept( self, api_client, dept_head, category, dept_it ): """Начальник отдела может создавать в своём отделе.""" api_client.force_authenticate(user=dept_head) data = valid_equipment_data(category, dept_it) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_201_CREATED def test_dept_head_cannot_create_in_other_dept( self, api_client, dept_head, category, dept_hr ): """Начальник отдела НЕ может создавать в чужом отделе.""" api_client.force_authenticate(user=dept_head) data = valid_equipment_data(category, dept_hr) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_403_FORBIDDEN def test_authorized_member_can_create_in_own_dept( self, api_client, authorized_member, category, dept_it ): """Уполномоченный сотрудник может создавать в своём отделе.""" api_client.force_authenticate(user=authorized_member) data = valid_equipment_data(category, dept_it) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_201_CREATED def test_authorized_member_cannot_create_in_other_dept( self, api_client, authorized_member, category, dept_hr ): """Уполномоченный НЕ может создавать в чужом отделе.""" api_client.force_authenticate(user=authorized_member) data = valid_equipment_data(category, dept_hr) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_403_FORBIDDEN def test_regular_member_cannot_create( self, api_client, dept_member, category, dept_it ): """Обычный сотрудник БЕЗ прав не может создавать.""" api_client.force_authenticate(user=dept_member) data = valid_equipment_data(category, dept_it) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_403_FORBIDDEN def test_random_user_cannot_create( self, api_client, random_user, category, dept_it ): """Пользователь без отдела не может создавать.""" api_client.force_authenticate(user=random_user) data = valid_equipment_data(category, dept_it) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_403_FORBIDDEN # ============================================================================= # Tests: Создание - выбор отдела # ============================================================================= @pytest.mark.django_db class TestEquipmentCreateDepartmentChoice: """Тесты: кто может выбирать отдел при создании.""" def test_admin_can_choose_any_department( self, api_client, admin_user, category, dept_it, dept_hr ): """Админ может указать любой отдел.""" api_client.force_authenticate(user=admin_user) # Создаём в IT отделе data1 = valid_equipment_data(category, dept_it, name="Оборудование IT") response1 = api_client.post(equipment_create_url(), data1, format="json") assert response1.status_code == status.HTTP_201_CREATED assert response1.data["department"] == dept_it.id # Создаём в HR отделе data2 = valid_equipment_data(category, dept_hr, name="Оборудование HR") response2 = api_client.post(equipment_create_url(), data2, format="json") assert response2.status_code == status.HTTP_201_CREATED assert response2.data["department"] == dept_hr.id def test_model_perm_user_can_choose_any_department( self, api_client, model_perm_user, category, dept_it, dept_hr ): """Пользователь с модельными правами может указать любой отдел.""" api_client.force_authenticate(user=model_perm_user) data = valid_equipment_data(category, dept_hr) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_201_CREATED assert response.data["department"] == dept_hr.id def test_dept_head_department_is_forced_to_own( self, api_client, dept_head, category, dept_it, dept_hr ): """Начальник отдела: попытка указать чужой отдел → запрет или замена на свой.""" api_client.force_authenticate(user=dept_head) # Пытаемся создать в HR отделе, но мы начальник IT data = valid_equipment_data(category, dept_hr) response = api_client.post(equipment_create_url(), data, format="json") # Должно быть либо 403, либо автозамена на свой отдел # Выбираем вариант с 403 assert response.status_code == status.HTTP_403_FORBIDDEN def test_authorized_member_department_is_forced( self, api_client, authorized_member, category, dept_it, dept_hr ): """Уполномоченный: отдел автоматически = его отдел, попытка указать другой → ошибка.""" api_client.force_authenticate(user=authorized_member) data = valid_equipment_data(category, dept_hr) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_403_FORBIDDEN # ============================================================================= # Tests: Создание - выбор ответственного # ============================================================================= @pytest.mark.django_db class TestEquipmentCreateResponsibleChoice: """Тесты: кто может выбирать ответственного при создании.""" def test_admin_can_choose_any_responsible_in_dept( self, api_client, admin_user, category, dept_it, dept_member ): """Админ может указать любого сотрудника из отдела как ответственного.""" api_client.force_authenticate(user=admin_user) data = valid_equipment_data(category, dept_it, responsible=dept_member) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_201_CREATED assert response.data["responsible_person"] == dept_member.id def test_dept_head_can_choose_responsible_from_own_dept( self, api_client, dept_head, category, dept_it, dept_member ): """Начальник может выбрать ответственного из своего отдела.""" api_client.force_authenticate(user=dept_head) data = valid_equipment_data(category, dept_it, responsible=dept_member) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_201_CREATED assert response.data["responsible_person"] == dept_member.id def test_dept_head_default_responsible_is_self( self, api_client, dept_head, category, dept_it ): """Начальник: если ответственный не указан, по умолчанию он сам.""" api_client.force_authenticate(user=dept_head) data = valid_equipment_data(category, dept_it) # Не указываем responsible_person response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_201_CREATED assert response.data["responsible_person"] == dept_head.id def test_authorized_member_responsible_is_forced_to_head( self, api_client, authorized_member, category, dept_it, dept_head, dept_member ): """Уполномоченный: ответственный автоматически = начальник, нельзя выбрать другого.""" api_client.force_authenticate(user=authorized_member) # Пытаемся указать другого ответственного data = valid_equipment_data(category, dept_it, responsible=dept_member) response = api_client.post(equipment_create_url(), data, format="json") # Должно либо заменить на начальника, либо вернуть ошибку if response.status_code == status.HTTP_201_CREATED: # Автозамена на начальника assert response.data["responsible_person"] == dept_head.id else: # Или ошибка валидации assert response.status_code == status.HTTP_400_BAD_REQUEST def test_authorized_member_default_responsible_is_head( self, api_client, authorized_member, category, dept_it, dept_head ): """Уполномоченный: если ответственный не указан, это начальник.""" api_client.force_authenticate(user=authorized_member) data = valid_equipment_data(category, dept_it) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_201_CREATED assert response.data["responsible_person"] == dept_head.id # ============================================================================= # Tests: Валидация ответственного - принадлежность к отделу # ============================================================================= @pytest.mark.django_db class TestEquipmentResponsibleValidation: """Тесты: ответственный должен состоять в отделе оборудования.""" def test_responsible_must_be_in_department( self, api_client, admin_user, category, dept_it, other_dept_head ): """Ответственный ДОЛЖЕН быть в том же отделе, что и оборудование.""" api_client.force_authenticate(user=admin_user) # other_dept_head состоит в HR, а мы создаём в IT data = valid_equipment_data(category, dept_it, responsible=other_dept_head) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_400_BAD_REQUEST assert "responsible_person" in response.data or "responsible" in str( response.data ) def test_responsible_in_same_department_is_valid( self, api_client, admin_user, category, dept_it, dept_member ): """Ответственный из того же отдела — валидно.""" api_client.force_authenticate(user=admin_user) data = valid_equipment_data(category, dept_it, responsible=dept_member) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_201_CREATED def test_dept_head_is_valid_responsible( self, api_client, admin_user, category, dept_it, dept_head ): """Начальник отдела — валидный ответственный.""" api_client.force_authenticate(user=admin_user) data = valid_equipment_data(category, dept_it, responsible=dept_head) response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_201_CREATED # ============================================================================= # Tests: Изменение оборудования # ============================================================================= @pytest.mark.django_db class TestEquipmentUpdateAccess: """Тесты: кто может изменять оборудование.""" def test_admin_can_update_any_equipment( self, api_client, admin_user, category, dept_it, dept_head ): """Админ может изменять любое оборудование.""" equipment = make_equipment("Laptop", dept_it, category, dept_head) api_client.force_authenticate(user=admin_user) response = api_client.patch( equipment_detail_url(equipment.id), {"name": "Updated Laptop"}, format="json", ) assert response.status_code == status.HTTP_200_OK assert response.data["name"] == "Updated Laptop" def test_inactive_current_responsible_does_not_block_update( self, api_client, admin_user, category, dept_it, dept_member ): """Карточку можно править после увольнения текущего ответственного.""" equipment = make_equipment("Legacy laptop", dept_it, category, dept_member) dept_member.is_active = False dept_member.save(update_fields=["is_active"]) EmployeeDepartment.objects.filter( employee=dept_member, department=dept_it, ).update(is_active=False) api_client.force_authenticate(user=admin_user) response = api_client.patch( equipment_detail_url(equipment.id), {"name": "Updated legacy laptop"}, format="json", ) assert response.status_code == status.HTTP_200_OK, response.content equipment.refresh_from_db() assert equipment.responsible_person_id == dept_member.id def test_cannot_assign_inactive_responsible( self, api_client, admin_user, category, dept_it, dept_head, dept_member ): """Новым ответственным нельзя назначить уволенного сотрудника.""" equipment = make_equipment("Active laptop", dept_it, category, dept_head) dept_member.is_active = False dept_member.save(update_fields=["is_active"]) api_client.force_authenticate(user=admin_user) response = api_client.patch( equipment_detail_url(equipment.id), {"responsible_person": dept_member.id}, format="json", ) assert response.status_code == status.HTTP_400_BAD_REQUEST assert "responsible_person" in response.data def test_model_perm_user_can_update_any( self, api_client, model_perm_user, category, dept_it, dept_head ): """Пользователь с модельными правами может изменять любое оборудование.""" equipment = make_equipment("Laptop", dept_it, category, dept_head) api_client.force_authenticate(user=model_perm_user) response = api_client.patch( equipment_detail_url(equipment.id), {"name": "Updated"}, format="json" ) assert response.status_code == status.HTTP_200_OK def test_dept_head_can_update_own_dept_equipment( self, api_client, dept_head, category, dept_it ): """Начальник отдела может изменять оборудование своего отдела.""" equipment = make_equipment("Laptop", dept_it, category, dept_head) api_client.force_authenticate(user=dept_head) response = api_client.patch( equipment_detail_url(equipment.id), {"name": "Updated"}, format="json" ) assert response.status_code == status.HTTP_200_OK def test_dept_head_cannot_update_other_dept_equipment( self, api_client, dept_head, category, dept_hr, other_dept_head ): """Начальник НЕ может изменять оборудование чужого отдела.""" equipment = make_equipment("Laptop HR", dept_hr, category, other_dept_head) api_client.force_authenticate(user=dept_head) response = api_client.patch( equipment_detail_url(equipment.id), {"name": "Hacked"}, format="json" ) assert response.status_code == status.HTTP_403_FORBIDDEN def test_authorized_member_can_update_own_dept( self, api_client, authorized_member, category, dept_it, dept_head ): """Уполномоченный может изменять оборудование своего отдела.""" equipment = make_equipment("Laptop", dept_it, category, dept_head) api_client.force_authenticate(user=authorized_member) response = api_client.patch( equipment_detail_url(equipment.id), {"name": "Updated by authorized"}, format="json", ) assert response.status_code == status.HTTP_200_OK def test_responsible_person_can_update( self, api_client, dept_member, category, dept_it ): """Ответственный может изменять своё оборудование.""" equipment = make_equipment("Laptop", dept_it, category, dept_member) api_client.force_authenticate(user=dept_member) response = api_client.patch( equipment_detail_url(equipment.id), {"location": "Офис 305"}, format="json" ) assert response.status_code == status.HTTP_200_OK def test_regular_member_cannot_update_not_their_equipment( self, api_client, dept_member, category, dept_it, dept_head ): """Обычный сотрудник НЕ может изменять чужое оборудование.""" equipment = make_equipment("Laptop", dept_it, category, dept_head) api_client.force_authenticate(user=dept_member) response = api_client.patch( equipment_detail_url(equipment.id), {"name": "Hacked"}, format="json" ) assert response.status_code == status.HTTP_403_FORBIDDEN # ============================================================================= # Tests: Удаление оборудования # ============================================================================= @pytest.mark.django_db class TestEquipmentDeleteAccess: """Тесты: кто может удалять оборудование.""" def test_admin_can_delete( self, api_client, admin_user, category, dept_it, dept_head ): """Админ может удалять любое оборудование.""" equipment = make_equipment("Laptop", dept_it, category, dept_head) api_client.force_authenticate(user=admin_user) response = api_client.delete(equipment_detail_url(equipment.id)) assert response.status_code == status.HTTP_204_NO_CONTENT def test_model_perm_user_can_delete( self, api_client, model_perm_user, category, dept_it, dept_head ): """Пользователь с модельными правами может удалять.""" equipment = make_equipment("Laptop", dept_it, category, dept_head) api_client.force_authenticate(user=model_perm_user) response = api_client.delete(equipment_detail_url(equipment.id)) assert response.status_code == status.HTTP_204_NO_CONTENT def test_dept_head_can_delete_own_dept_equipment( self, api_client, dept_head, category, dept_it ): """Начальник может удалять оборудование своего отдела.""" equipment = make_equipment("Laptop", dept_it, category, dept_head) api_client.force_authenticate(user=dept_head) response = api_client.delete(equipment_detail_url(equipment.id)) assert response.status_code == status.HTTP_204_NO_CONTENT def test_dept_head_cannot_delete_other_dept( self, api_client, dept_head, category, dept_hr, other_dept_head ): """Начальник НЕ может удалять оборудование чужого отдела.""" equipment = make_equipment("Laptop", dept_hr, category, other_dept_head) api_client.force_authenticate(user=dept_head) response = api_client.delete(equipment_detail_url(equipment.id)) assert response.status_code == status.HTTP_403_FORBIDDEN def test_authorized_member_cannot_delete( self, api_client, authorized_member, category, dept_it, dept_head ): """Уполномоченный НЕ может удалять оборудование (только создание/изменение).""" equipment = make_equipment("Laptop", dept_it, category, dept_head) api_client.force_authenticate(user=authorized_member) response = api_client.delete(equipment_detail_url(equipment.id)) # Уполномоченный может только создавать и изменять, но не удалять assert response.status_code == status.HTTP_403_FORBIDDEN def test_responsible_cannot_delete( self, api_client, dept_member, category, dept_it ): """Ответственный НЕ может удалять своё оборудование.""" equipment = make_equipment("Laptop", dept_it, category, dept_member) api_client.force_authenticate(user=dept_member) response = api_client.delete(equipment_detail_url(equipment.id)) assert response.status_code == status.HTTP_403_FORBIDDEN # ============================================================================= # Tests: Просмотр оборудования # ============================================================================= @pytest.mark.django_db class TestEquipmentViewAccess: """Тесты: просмотр оборудования.""" def test_unauthenticated_cannot_view( self, api_client, category, dept_it, dept_head ): """Неаутентифицированный не может просматривать.""" equipment = make_equipment("Laptop", dept_it, category, dept_head) response = api_client.get(equipment_detail_url(equipment.id)) assert response.status_code == status.HTTP_401_UNAUTHORIZED def test_any_authenticated_can_view_list( self, api_client, dept_member, category, dept_it, dept_head ): """Любой аутентифицированный сотрудник может просматривать список.""" make_equipment("Laptop", dept_it, category, dept_head) api_client.force_authenticate(user=dept_member) response = api_client.get(equipment_create_url()) assert response.status_code == status.HTTP_200_OK def test_any_authenticated_can_view_detail( self, api_client, random_user, category, dept_it, dept_head ): """Любой аутентифицированный может просматривать детали.""" equipment = make_equipment("Laptop", dept_it, category, dept_head) api_client.force_authenticate(user=random_user) response = api_client.get(equipment_detail_url(equipment.id)) assert response.status_code == status.HTTP_200_OK # ============================================================================= # Tests: API для получения доступных опций # ============================================================================= @pytest.mark.django_db class TestEquipmentCreateOptions: """Тесты: эндпоинт для получения доступных опций при создании.""" def test_admin_gets_all_departments(self, api_client, admin_user, dept_it, dept_hr): """Админ получает все отделы для выбора.""" api_client.force_authenticate(user=admin_user) url = reverse("api:v1:procurement:equipment-create-options") response = api_client.get(url) assert response.status_code == status.HTTP_200_OK assert response.data["can_choose_department"] is True assert response.data["can_choose_responsible"] is True dept_ids = [d["id"] for d in response.data["allowed_departments"]] assert dept_it.id in dept_ids assert dept_hr.id in dept_ids def test_dept_head_gets_only_own_department( self, api_client, dept_head, dept_it, dept_hr ): """Начальник получает только свой отдел.""" api_client.force_authenticate(user=dept_head) url = reverse("api:v1:procurement:equipment-create-options") response = api_client.get(url) assert response.status_code == status.HTTP_200_OK assert response.data["can_choose_department"] is False assert response.data["can_choose_responsible"] is True dept_ids = [d["id"] for d in response.data["allowed_departments"]] assert dept_ids == [dept_it.id] def test_authorized_member_gets_limited_options( self, api_client, authorized_member, dept_it, dept_head ): """Уполномоченный получает свой отдел и НЕ может выбирать ответственного.""" api_client.force_authenticate(user=authorized_member) url = reverse("api:v1:procurement:equipment-create-options") response = api_client.get(url) assert response.status_code == status.HTTP_200_OK assert response.data["can_choose_department"] is False assert response.data["can_choose_responsible"] is False assert response.data["default_responsible"]["id"] == dept_head.id def test_regular_member_cannot_access(self, api_client, dept_member): """Обычный сотрудник без прав не получает опции (или получает пустой ответ).""" api_client.force_authenticate(user=dept_member) url = reverse("api:v1:procurement:equipment-create-options") response = api_client.get(url) # Либо 403, либо пустой список отделов if response.status_code == status.HTTP_200_OK: assert response.data["allowed_departments"] == [] else: assert response.status_code == status.HTTP_403_FORBIDDEN # ============================================================================= # Tests: Массовое создание # ============================================================================= @pytest.mark.django_db class TestEquipmentBulkCreate: """Тесты: массовое создание оборудования.""" def test_admin_can_bulk_create( self, api_client, admin_user, category, dept_it, dept_head ): """Админ может массово создавать.""" api_client.force_authenticate(user=admin_user) data = valid_equipment_data(category, dept_it, responsible=dept_head) data["quantity"] = 3 response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_201_CREATED assert response.data["created_count"] == 3 def test_bulk_create_generates_unique_inventory_numbers( self, api_client, admin_user, category, dept_it, dept_head ): """При массовом создании генерируются уникальные инвентарные номера.""" api_client.force_authenticate(user=admin_user) data = valid_equipment_data(category, dept_it, responsible=dept_head) data["quantity"] = 3 response = api_client.post(equipment_create_url(), data, format="json") inventory_numbers = [ eq["inventory_number"] for eq in response.data["equipment"] ] assert len(set(inventory_numbers)) == 3 # Все уникальные def test_authorized_member_bulk_create_uses_dept_head( self, api_client, authorized_member, category, dept_it, dept_head ): """Уполномоченный при массовом создании — ответственный всегда начальник.""" api_client.force_authenticate(user=authorized_member) data = valid_equipment_data(category, dept_it) data["quantity"] = 2 response = api_client.post(equipment_create_url(), data, format="json") assert response.status_code == status.HTTP_201_CREATED for eq in response.data["equipment"]: assert eq["responsible_person"] == dept_head.id