From 4c398856aedc2fbfdb90295e7455337ab7584938 Mon Sep 17 00:00:00 2001 From: igor_nadein Date: Wed, 21 Jan 2026 13:13:36 +0300 Subject: [PATCH] Update API tests: improve fixtures and LDAP optional mode support --- backend/tests/api/v1/calendar_app/conftest.py | 17 ------ .../api/v1/documents/test_documents_api.py | 57 +------------------ .../employees/test_department_head_rights.py | 19 ++----- .../test_department_membership_separation.py | 21 +------ .../api/v1/employees/test_department_roles.py | 20 +------ .../employees/test_department_roles_extra.py | 25 +------- .../api/v1/employees/test_departments.py | 34 +---------- .../test_email_verification_security.py | 9 +-- .../tests/api/v1/employees/test_employees.py | 26 +-------- .../test_employees_fields_in_list.py | 15 +---- .../v1/employees/test_ldap_optional_groups.py | 22 +------ .../employees/test_ldap_optional_register.py | 15 +---- .../tests/api/v1/employees/test_positions.py | 8 +-- .../api/v1/employees/test_role_assignment.py | 15 +---- backend/tests/api/v1/employees/test_skills.py | 23 ++------ backend/tests/api/v1/feed/test_comments.py | 16 +----- backend/tests/api/v1/feed/test_posts.py | 44 +------------- backend/tests/api/v1/requests_app/conftest.py | 9 --- .../api/v1/requests_app/test_requests_api.py | 4 +- .../requests_app/test_requests_departments.py | 6 +- 20 files changed, 49 insertions(+), 356 deletions(-) diff --git a/backend/tests/api/v1/calendar_app/conftest.py b/backend/tests/api/v1/calendar_app/conftest.py index 373ccab1..cd6a076f 100644 --- a/backend/tests/api/v1/calendar_app/conftest.py +++ b/backend/tests/api/v1/calendar_app/conftest.py @@ -19,10 +19,8 @@ from rest_framework.test import APIClient API_EVENTS = "/api/v1/calendar/events/" - _phone_counter = itertools.count(100_000_000) # 9 цифр - def _detect_phone_field(User: type[models.Model]) -> str | None: """Возвращает имя телефонного поля модели пользователя, если оно есть. @@ -43,7 +41,6 @@ def _detect_phone_field(User: type[models.Model]) -> str | None: continue return None - def _next_ru_phone() -> str: """Генерирует уникальный российский номер в формате E.164. @@ -54,7 +51,6 @@ def _next_ru_phone() -> str: """ return f"+79{next(_phone_counter):09d}" - @pytest.fixture def api_url() -> str: """Базовый URL API календаря. @@ -64,7 +60,6 @@ def api_url() -> str: """ return API_EVENTS - @pytest.fixture def User() -> Type[models.Model]: """Модель пользователя проекта. @@ -74,7 +69,6 @@ def User() -> Type[models.Model]: """ return get_user_model() - @pytest.fixture def CalendarEvent() -> Type[models.Model]: """Модель событий календаря. @@ -87,7 +81,6 @@ def CalendarEvent() -> Type[models.Model]: """ return django_apps.get_model("calendar_app", "CalendarEvent") - @pytest.fixture def Recurrence(): """Enum повторяемости из приложения (это не модель).""" @@ -95,7 +88,6 @@ def Recurrence(): return _Recurrence - @pytest.fixture def Department() -> Optional[Type[models.Model]]: """Модель отдела. @@ -108,7 +100,6 @@ def Department() -> Optional[Type[models.Model]]: except LookupError: return None - @pytest.fixture def make_user() -> callable: """Фабрика пользователей, совместимая с кастомной моделью, где телефон обязателен. @@ -173,25 +164,21 @@ def make_user() -> callable: return _make_user - @pytest.fixture def admin_user(make_user) -> models.Model: """Администратор (superuser).""" return make_user("admin@example.com", is_staff=True, is_superuser=True) - @pytest.fixture def regular_user(make_user) -> models.Model: """Обычный пользователь без спецправ.""" return make_user("regular@example.com") - @pytest.fixture def dept_manager_user(make_user) -> models.Model: """Пользователь, которому потом выдадим право manage_department_events.""" return make_user("manager@example.com") - @pytest.fixture def give_manage_calendar_perm(db) -> Callable[[DjangoModel, DjangoModel], None]: """Возвращает функцию, которая назначает пользователю ролевое право управления календарём в указанном отделе. @@ -247,13 +234,11 @@ def give_manage_calendar_perm(db) -> Callable[[DjangoModel, DjangoModel], None]: return _grant - @pytest.fixture def api_client() -> APIClient: """Чистый DRF-клиент без авторизации.""" return APIClient() - @pytest.fixture def auth_client() -> Callable[[models.Model], APIClient]: """Создаёт авторизованный DRF-клиент. @@ -272,7 +257,6 @@ def auth_client() -> Callable[[models.Model], APIClient]: return _build - @pytest.fixture def make_department(Department) -> Callable[..., models.Model]: """Фабрика отделов. @@ -293,7 +277,6 @@ def make_department(Department) -> Callable[..., models.Model]: return _make_dept - @pytest.fixture def make_event(CalendarEvent, Recurrence) -> Callable[..., models.Model]: """Фабрика событий календаря. diff --git a/backend/tests/api/v1/documents/test_documents_api.py b/backend/tests/api/v1/documents/test_documents_api.py index f026e4e2..6c2da03d 100644 --- a/backend/tests/api/v1/documents/test_documents_api.py +++ b/backend/tests/api/v1/documents/test_documents_api.py @@ -13,7 +13,6 @@ from __future__ import annotations Все функции и классы снабжены докстрингами (Google-style). """ -import itertools import tempfile from typing import Iterable @@ -27,24 +26,14 @@ from django.utils import timezone from documents.models import Document, DocumentAcknowledgement from rest_framework import status from rest_framework.test import APIClient +from tests.conftest import _unique_phone pytestmark = pytest.mark.django_db User = get_user_model() # -------------- helpers / factories -------------- - -_phone_seq = itertools.count(1000) - - -def _unique_phone() -> str: - """Генерирует уникальный валидный E.164 номер в рамках сессии тестов. - - Returns: - str: Телефонный номер в формате +7999000xxx. - """ - return f"+7999000{next(_phone_seq):03d}" - +# _unique_phone импортируется из tests.conftest def _file(name: str = "doc.pdf", content: bytes = b"x") -> SimpleUploadedFile: """Создаёт минимальный загружаемый файл. @@ -60,7 +49,6 @@ def _file(name: str = "doc.pdf", content: bytes = b"x") -> SimpleUploadedFile: name=name, content=content, content_type="application/pdf" ) - def grant_perms(user: User, *codenames: str) -> None: """Выдаёт пользователю модельные права по кодовым именам из приложения documents. @@ -78,7 +66,6 @@ def grant_perms(user: User, *codenames: str) -> None: user.user_permissions.add(perm) user.save(update_fields=[]) - def make_document( *, title: str = "Doc", @@ -111,23 +98,7 @@ def make_document( doc.recipients.set(recipients) return doc - -@pytest.fixture -def make_user(user_factory): - """Фабрика пользователей для тестов API уровня документов. - - Args: - user_factory (Callable): Фабрика из tests/conftest.py. - - Returns: - Callable[..., User]: Функция создания Employee. - """ - - def _make(email: str, **extra) -> User: - return user_factory(email=email, **extra) - - return _make - +# make_user импортируется из tests.conftest @pytest.fixture def auth_client(auth_client_factory): @@ -141,7 +112,6 @@ def auth_client(auth_client_factory): """ return auth_client_factory - @pytest.fixture def api_urls(): """Возвращает именованные URL API документов. @@ -162,16 +132,13 @@ def api_urls(): "ack": _ack, } - @pytest.fixture(autouse=True) def enable_media_files(settings): settings.MEDIA_ROOT = tempfile.mkdtemp() settings.DEBUG = True - # -------------------- A. Роли и доступ -------------------- - class TestAuthAndPermissions: """A. Роли и доступ — статусы для разных ролей и прав.""" @@ -344,10 +311,8 @@ class TestAuthAndPermissions: # delete assert client.delete(api_urls["detail"](doc_id)).status_code in (200, 204) - # -------------------- B. Создание (POST) -------------------- - @pytest.mark.django_db class TestCreate: """B. Создание документа multipart/form-data.""" @@ -478,10 +443,8 @@ class TestCreate: assert active.id in ids assert inactive.id not in ids - # -------------------- C. Чтение (GET) -------------------- - class TestRead: """C. Чтение: list/detail, структура, пагинация, file_url.""" @@ -551,10 +514,8 @@ class TestRead: body["file_url"] == expected_url ), f"Ожидался URL: {expected_url}, получен: {body['file_url']}" - # -------------------- D. Обновление (PUT/PATCH) -------------------- - class TestUpdate: """D. Обновление документа (PUT/PATCH).""" @@ -648,10 +609,8 @@ class TestUpdate: d.refresh_from_db() assert list(d.recipients.values_list("id", flat=True)) == [c.id] - # -------------------- E. Удаление (DELETE) -------------------- - class TestDelete: """E. Удаление документа.""" @@ -679,10 +638,8 @@ class TestDelete: # каскад: записей нет assert not DocumentAcknowledgement.objects.filter(document=d).exists() - # -------------------- F. Экшен acknowledge -------------------- - class TestAcknowledge: """F. Экшен POST /{id}/acknowledge/ — идемпотентность и политика доступа.""" @@ -743,10 +700,8 @@ class TestAcknowledge: ) assert r.status_code == 403 - # -------------------- G. Сериализация и данные -------------------- - class TestSerialization: """G. Сериализация: uploaded_by/recipients поля и скачивание файла.""" @@ -788,10 +743,8 @@ class TestSerialization: # resp = client.get(file_url) # assert resp.status_code == 200 - # -------------------- H. Ошибки/краевые случаи -------------------- - class TestErrorsAndEdgeCases: """H. Неудачные Content-Type, perms и размер файла.""" @@ -855,10 +808,8 @@ class TestErrorsAndEdgeCases: # фиксируем текущий результат при отсутствии лимита assert r.status_code in (200, 201) - # -------------------- I. Производительность/фильтры -------------------- - class TestPerformance: """I. N+1 и пагинация.""" @@ -898,10 +849,8 @@ class TestPerformance: r3 = client.get(api_urls["list"], {"page": 3}) assert r3.status_code == 200 - # -------------------- J. Совместимость и безопасность -------------------- - class TestSecurityAndPolicy: """J. Базовая совместимость по авторизации и политика acknowledge.""" diff --git a/backend/tests/api/v1/employees/test_department_head_rights.py b/backend/tests/api/v1/employees/test_department_head_rights.py index 63633318..0204bada 100644 --- a/backend/tests/api/v1/employees/test_department_head_rights.py +++ b/backend/tests/api/v1/employees/test_department_head_rights.py @@ -6,21 +6,22 @@ from rest_framework.test import APIClient from rest_framework import status from employees.models import Department, DepartmentRole, EmployeeDepartment, Employee +from tests.conftest import _unique_phone +from tests.test_config import DEFAULT_PASSWORD User = get_user_model() - # ========================= # Helpers # ========================= - def _unique_phone() -> str: base = 79000000000 # 79 + 9 нулей return str(base + User.objects.count()) - +@pytest.fixture def make_user(email: str, staff: bool = False, verified: bool = True) -> User: + """Fixture для создания пользователей.""" u = User.objects.create_user( email=email, @@ -35,12 +36,10 @@ def make_user(email: str, staff: bool = False, verified: bool = True) -> User: u.save(update_fields=["is_active"]) return u - def _unique_email() -> str: base = "asd@mail.com" return str(Employee.objects.count()) + base - def _make_user(staff=False, superuser=False) -> Employee: """ Создаёт активного и 'email_verified' пользователя, чтобы не споткнуться о @@ -48,7 +47,7 @@ def _make_user(staff=False, superuser=False) -> Employee: """ u = Employee.objects.create_user( email=_unique_email(), - password="pass", + password=DEFAULT_PASSWORD, phone_number=_unique_phone(), send_activation_email=False, first_name="T", @@ -61,7 +60,6 @@ def _make_user(staff=False, superuser=False) -> Employee: u.save(update_fields=["is_staff", "is_superuser", "email_verified", "is_active"]) return u - def make_role( dept: Department, name: str, codes: list[str] | None = None ) -> DepartmentRole: @@ -70,7 +68,6 @@ def make_role( # но фабрика оставлена для удобства return role - def add_member( user: User, dept: Department, @@ -83,22 +80,18 @@ def add_member( defaults={"is_active": is_active, "role": role}, ) - # ========================= # Fixtures # ========================= - @pytest.fixture() def api_client() -> APIClient: return APIClient() - # ========================= # Tests # ========================= - @pytest.mark.django_db def test_head_can_update_department_without_explicit_role_perms(api_client: APIClient): head = make_user("head@example.com") @@ -113,7 +106,6 @@ def test_head_can_update_department_without_explicit_role_perms(api_client: APIC assert resp.status_code == status.HTTP_200_OK assert resp.json()["description"] == "new" - @pytest.mark.django_db def test_head_can_set_member_role_and_non_head_cannot(api_client: APIClient): head = make_user("head@example.com") @@ -141,7 +133,6 @@ def test_head_can_set_member_role_and_non_head_cannot(api_client: APIClient): resp = api_client.post(url, payload, format="json") assert resp.status_code == status.HTTP_403_FORBIDDEN - @pytest.mark.django_db def test_head_can_change_head_and_loses_rights_afterwards(api_client: APIClient): old_head = make_user("old@example.com") diff --git a/backend/tests/api/v1/employees/test_department_membership_separation.py b/backend/tests/api/v1/employees/test_department_membership_separation.py index 2dfd840d..caeecc5f 100644 --- a/backend/tests/api/v1/employees/test_department_membership_separation.py +++ b/backend/tests/api/v1/employees/test_department_membership_separation.py @@ -12,12 +12,10 @@ from employees.models import ( Employee, ) - # ========================= # Helpers # ========================= - def _unique_phone_from_email(email: str) -> str: """ Возвращает детерминированный уникальный номер телефона в формате E.164 @@ -27,8 +25,9 @@ def _unique_phone_from_email(email: str) -> str: # "+79" + 9 цифр = 11-значный номер РФ return f"+79{n:09d}" - +@pytest.fixture def make_user(email: str, *, staff: bool = False, superuser: bool = False) -> Employee: + """Fixture для создания пользователей.""" """ Фабрика пользователя для тестов. Учитывает ограничение уникальности phone_number. """ @@ -43,7 +42,6 @@ def make_user(email: str, *, staff: bool = False, superuser: bool = False) -> Em is_active=True, ) - def ensure_dept_perm(code: str, name: str | None = None) -> DepartmentPermission: """ Создаёт (или возвращает) департаментское право с данным кодом. @@ -52,7 +50,6 @@ def ensure_dept_perm(code: str, name: str | None = None) -> DepartmentPermission code=code, defaults={"name": name or code} )[0] - def make_role( dept: Department, name: str, codes: list[str] | None = None ) -> DepartmentRole: @@ -65,7 +62,6 @@ def make_role( role.scoped_permissions.add(*perms) return role - def grant_manage_in_dept(user: Employee, dept: Department) -> DepartmentRole: """ Выдаёт пользователю роль в отделе с правом manage_department. @@ -78,7 +74,6 @@ def grant_manage_in_dept(user: Employee, dept: Department) -> DepartmentRole: ) return role - def grant_assign_in_dept(user: Employee, dept: Department) -> DepartmentRole: """ Выдаёт пользователю роль в отделе с правом assign_department_role. @@ -91,28 +86,23 @@ def grant_assign_in_dept(user: Employee, dept: Department) -> DepartmentRole: ) return role - # Утилиты URL с обязательным хвостовым слэшем (APPEND_SLASH=True) def url_add_member(dept_id: int) -> str: """POST /api/v1/departments/{id}/add_member/""" return f"/api/v1/departments/{dept_id}/add_member/" - def url_remove_member(dept_id: int) -> str: """POST /api/v1/departments/{id}/remove_member/""" return f"/api/v1/departments/{dept_id}/remove_member/" - def url_set_member_role(dept_id: int) -> str: """POST /api/v1/departments/{id}/set_member_role/""" return f"/api/v1/departments/{dept_id}/set_member_role/" - # ========================= # Tests: add_member / remove_member # ========================= - @pytest.mark.django_db def test_add_member_requires_manage_and_does_not_assign_role(api_client: APIClient): """ @@ -148,7 +138,6 @@ def test_add_member_requires_manage_and_does_not_assign_role(api_client: APIClie assert link.is_active is True assert link.role_id is None # add_member не должен трогать роль - @pytest.mark.django_db def test_all_three_actions_require_auth_and_permissions(api_client: APIClient): """ @@ -241,12 +230,10 @@ def test_all_three_actions_require_auth_and_permissions(api_client: APIClient): == 403 ) - # ========================= # Tests: set_member_role (строгое разделение) # ========================= - @pytest.mark.django_db def test_set_member_role_requires_assign_not_manage(api_client: APIClient): """ @@ -284,7 +271,6 @@ def test_set_member_role_requires_assign_not_manage(api_client: APIClient): link = EmployeeDepartment.objects.get(employee=worker, department=d) assert link.role_id == role.id - @pytest.mark.django_db def test_set_member_role_does_not_create_membership_and_does_not_toggle_active( api_client: APIClient, @@ -329,12 +315,10 @@ def test_set_member_role_does_not_create_membership_and_does_not_toggle_active( assert link.is_active is True, "set_member_role не должен менять is_active" assert link.role_id == role.id - # ========================= # Доп. тест: авторизация и отсутствие пересечений # ========================= - @pytest.mark.django_db def test_all_three_actions_require_auth_and_permissions(api_client: APIClient): """ @@ -427,7 +411,6 @@ def test_all_three_actions_require_auth_and_permissions(api_client: APIClient): == 403 ) - @pytest.mark.django_db def test_add_member_ignores_role_parameter(api_client: APIClient): """ diff --git a/backend/tests/api/v1/employees/test_department_roles.py b/backend/tests/api/v1/employees/test_department_roles.py index ef268c1e..4f20e210 100644 --- a/backend/tests/api/v1/employees/test_department_roles.py +++ b/backend/tests/api/v1/employees/test_department_roles.py @@ -11,22 +11,15 @@ from employees.models import ( DepartmentPermission, EmployeeDepartment, # <-- если у тебя EmployeeDepartmentLink, замени импорт и упоминания ниже ) +from tests.conftest import _unique_phone User = get_user_model() - # ========================= # Helpers # ========================= -def _unique_phone() -> str: - """ - Делает уникальный 11-значный номер: 79xxxxxxxxx - Используем текущее количество пользователей как инкремент. - """ - base = 79000000000 # 79 + 9 нулей - return str(base + User.objects.count()) - +@pytest.fixture def make_user(email: str, staff: bool = False, verified: bool = True) -> User: """ Создаёт пользователя с обязательным phone_number и отключённой отправкой активационного письма. @@ -70,7 +63,6 @@ def grant_assign_in_dept(user: User, dept: Department) -> DepartmentRole: ) return role - # ========================= # Fixtures # ========================= @@ -79,7 +71,6 @@ def grant_assign_in_dept(user: User, dept: Department) -> DepartmentRole: def api_client() -> APIClient: return APIClient() - # ========================= # Tests # ========================= @@ -114,7 +105,6 @@ def test_list_filtered_by_department(api_client: APIClient): ids = {it["id"] for it in items} assert ids == {r1.id, r2.id} - @pytest.mark.django_db def test_create_requires_assign_department_role(api_client: APIClient): d = Department.objects.create(name="Dept") @@ -138,7 +128,6 @@ def test_create_requires_assign_department_role(api_client: APIClient): # back-compat поле присутствует assert "permissions" in data and isinstance(data["permissions"], list) - @pytest.mark.django_db def test_update_and_destroy_scope_enforced(api_client: APIClient): d1 = Department.objects.create(name="Dept1") @@ -169,7 +158,6 @@ def test_update_and_destroy_scope_enforced(api_client: APIClient): resp = api_client.delete(url_detail_other) assert resp.status_code in (status.HTTP_204_NO_CONTENT, status.HTTP_200_OK) - @pytest.mark.django_db def test_perm_choices_and_perms_and_set_perms(api_client: APIClient): # засеем три стандартных кода @@ -219,7 +207,6 @@ def test_perm_choices_and_perms_and_set_perms(api_client: APIClient): role.refresh_from_db() assert role.scoped_permissions.count() == 0 - @pytest.mark.django_db def test_set_perms_validates_payload(api_client: APIClient): d = Department.objects.create(name="Dept") @@ -238,7 +225,6 @@ def test_set_perms_validates_payload(api_client: APIClient): resp = api_client.post(url_set, {"permission_ids": [999999]}, format="json") assert resp.status_code == 400 - @pytest.mark.django_db def test_back_compat_permissions_field_contains_ids(api_client: APIClient): d = Department.objects.create(name="Dept") @@ -257,7 +243,6 @@ def test_back_compat_permissions_field_contains_ids(api_client: APIClient): data = resp.json() assert set(data["permissions"]) == {dp_manage.id, dp_assign.id} - @pytest.mark.django_db def test_create_with_scoped_permission_codes_and_update_name(api_client: APIClient): d = Department.objects.create(name="Dept") @@ -288,7 +273,6 @@ def test_create_with_scoped_permission_codes_and_update_name(api_client: APIClie assert resp.status_code == 200 assert resp.json()["name"] == "Lead+1" - @pytest.mark.django_db def test_cross_department_access_denied_for_set_perms(api_client: APIClient): d1 = Department.objects.create(name="Dept1") diff --git a/backend/tests/api/v1/employees/test_department_roles_extra.py b/backend/tests/api/v1/employees/test_department_roles_extra.py index f701e1de..f7c2ab40 100644 --- a/backend/tests/api/v1/employees/test_department_roles_extra.py +++ b/backend/tests/api/v1/employees/test_department_roles_extra.py @@ -11,21 +11,17 @@ from employees.models import ( DepartmentPermission, EmployeeDepartment, # если у вас EmployeeDepartmentLink, замените импорт и упоминания ниже ) +from tests.conftest import _unique_phone User = get_user_model() - # ========================= # Helpers # ========================= - -def _unique_phone() -> str: - base = 79000000000 # 79 + 9 нулей - return str(base + User.objects.count()) - - +@pytest.fixture def make_user(email: str, staff: bool = False, verified: bool = True) -> User: + """Fixture для создания пользователей.""" extra = { "phone_number": _unique_phone(), "is_staff": staff, @@ -39,13 +35,11 @@ def make_user(email: str, staff: bool = False, verified: bool = True) -> User: pass return User.objects.create_user(email=email, password="pwd12345", **extra) - def ensure_perm(code: str, name: str | None = None) -> DepartmentPermission: return DepartmentPermission.objects.get_or_create( code=code, defaults={"name": name or code} )[0] - def make_role( dept: Department, name: str, codes: list[str] | None = None ) -> DepartmentRole: @@ -55,7 +49,6 @@ def make_role( role.scoped_permissions.add(*perms) return role - def grant_assign_in_dept(user: User, dept: Department) -> DepartmentRole: role = make_role(dept, "assigner", ["assign_department_role"]) EmployeeDepartment.objects.update_or_create( @@ -65,29 +58,24 @@ def grant_assign_in_dept(user: User, dept: Department) -> DepartmentRole: ) return role - # ========================= # Fixtures # ========================= - @pytest.fixture() def api_client() -> APIClient: return APIClient() - # ========================= # Tests # ========================= - @pytest.mark.django_db def test_unauth_cannot_list_or_get(): client = APIClient() url_list = reverse("api:v1:department-roles-list") assert client.get(url_list).status_code in (401, 403) - @pytest.mark.django_db def test_read_only_allowed_without_assign(api_client: APIClient): d = Department.objects.create(name="Dept") @@ -106,7 +94,6 @@ def test_read_only_allowed_without_assign(api_client: APIClient): url_detail = reverse("api:v1:department-roles-detail", args=[r.id]) assert api_client.get(url_detail).status_code == 200 - @pytest.mark.django_db def test_staff_override_for_write(api_client: APIClient): d = Department.objects.create(name="Dept") @@ -138,7 +125,6 @@ def test_staff_override_for_write(api_client: APIClient): == 200 ) - @pytest.mark.django_db def test_ordering_stable_by_name_then_id(api_client: APIClient): d1 = Department.objects.create(name="Dept1") @@ -163,7 +149,6 @@ def test_ordering_stable_by_name_then_id(api_client: APIClient): assert picked[0] < picked[1] # тай-брейк по id для одинаковых name assert picked[2] == r3.id - @pytest.mark.django_db def test_set_perms_ids_take_precedence_over_codes(api_client: APIClient): d = Department.objects.create(name="Dept") @@ -190,7 +175,6 @@ def test_set_perms_ids_take_precedence_over_codes(api_client: APIClient): codes = set(role.scoped_permissions.values_list("code", flat=True)) assert codes == {"manage_department"} # ids приоритетнее - @pytest.mark.django_db def test_serializer_codes_override_scoped_permissions_on_create(api_client: APIClient): d = Department.objects.create(name="Dept") @@ -222,7 +206,6 @@ def test_serializer_codes_override_scoped_permissions_on_create(api_client: APIC codes = set(role.scoped_permissions.values_list("code", flat=True)) assert codes == {"assign_department_role"} - @pytest.mark.django_db def test_perm_choices_fields_and_nonempty(api_client: APIClient): ensure_perm("manage_department", "Управлять") @@ -237,7 +220,6 @@ def test_perm_choices_fields_and_nonempty(api_client: APIClient): results = resp.json().get("results", []) assert results and {"id", "code", "name"}.issubset(results[0].keys()) - @pytest.mark.django_db def test_destroy_requires_assign_in_same_department(api_client: APIClient): d1 = Department.objects.create(name="D1") @@ -259,7 +241,6 @@ def test_destroy_requires_assign_in_same_department(api_client: APIClient): grant_assign_in_dept(u, d2) assert api_client.delete(url).status_code in (204, 200) - @pytest.mark.django_db def test_set_perms_dedup_and_replace_semantics(api_client: APIClient): d = Department.objects.create(name="Dept") diff --git a/backend/tests/api/v1/employees/test_departments.py b/backend/tests/api/v1/employees/test_departments.py index bf88ea97..b9d60ca5 100644 --- a/backend/tests/api/v1/employees/test_departments.py +++ b/backend/tests/api/v1/employees/test_departments.py @@ -58,6 +58,7 @@ from employees.models import ( ) from rest_framework import status from rest_framework.test import APIClient +from tests.conftest import _unique_phone pytestmark = pytest.mark.django_db @@ -65,23 +66,21 @@ User = get_user_model() # ---------- fixtures / helpers ---------- - @pytest.fixture def api_client(): return APIClient() - _phone_seq = itertools.count(1000) - def _unique_phone() -> str: # +7999000XXX — валидный E.164, и всегда уникальный в рамках тестов return f"+7999000{next(_phone_seq):03d}" - +@pytest.fixture def make_user( email: str, *, staff=False, superuser=False, verified=True, active=True, **extra ): + """Fixture для создания пользователей.""" """ Создаём пользователя напрямую (без менеджера, чтобы не отправлять почту). """ @@ -100,7 +99,6 @@ def make_user( u.save() return u - def perm_for_department(code: str) -> DepartmentPermission: """Безопасно получаем/создаём скоуп-право отдела по коду.""" p, _ = DepartmentPermission.objects.get_or_create( @@ -108,7 +106,6 @@ def perm_for_department(code: str) -> DepartmentPermission: ) return p - def make_role( dept: Department, name="mgr", codes: list[str] | None = None ) -> DepartmentRole: @@ -117,22 +114,18 @@ def make_role( r.scoped_permissions.add(*[perm_for_department(c) for c in codes]) return r - def extract_results(data): if isinstance(data, dict) and "results" in data: return data["results"] return data - # ---------- tests: auth/list/basic ---------- - def test_list_requires_auth(api_client: APIClient): url = reverse("api:v1:departments-list") resp = api_client.get(url) assert resp.status_code == status.HTTP_401_UNAUTHORIZED - def test_list_ok_for_authenticated(api_client: APIClient): user = make_user("u@example.com") api_client.force_authenticate(user=user) @@ -147,7 +140,6 @@ def test_list_ok_for_authenticated(api_client: APIClient): assert isinstance(items, list) assert len(items) == 2 - def test_search_and_ordering(api_client: APIClient): user = make_user("u@example.com") api_client.force_authenticate(user=user) @@ -174,10 +166,8 @@ def test_search_and_ordering(api_client: APIClient): names = [d["name"] for d in extract_results(resp.json())] assert names == ["Alpha", "Beta", "Gamma"] - # ---------- tests: employees_count annotation ---------- - def test_employees_count_adds_head_if_not_in_links(api_client: APIClient): user = make_user("u@example.com") api_client.force_authenticate(user=user) @@ -196,7 +186,6 @@ def test_employees_count_adds_head_if_not_in_links(api_client: APIClient): data = resp.json() assert data["employees_count"] == 3 # 2 + head вне связей - def test_employees_count_not_double_count_head_if_in_links(api_client: APIClient): user = make_user("u@example.com") api_client.force_authenticate(user=user) @@ -215,10 +204,8 @@ def test_employees_count_not_double_count_head_if_in_links(api_client: APIClient data = resp.json() assert data["employees_count"] == 3 - # ---------- tests: create/destroy ---------- - def test_create_requires_staff(api_client: APIClient): user = make_user("u@example.com") api_client.force_authenticate(user=user) @@ -236,7 +223,6 @@ def test_create_requires_staff(api_client: APIClient): assert resp.status_code == status.HTTP_201_CREATED assert Department.objects.filter(name="New").exists() - def test_destroy_requires_staff(api_client: APIClient): d = Department.objects.create(name="X") url = reverse("api:v1:departments-detail", args=[d.pk]) @@ -252,10 +238,8 @@ def test_destroy_requires_staff(api_client: APIClient): assert resp.status_code == status.HTTP_204_NO_CONTENT assert not Department.objects.filter(pk=d.pk).exists() - # ---------- tests: update/partial_update perms ---------- - def test_partial_update_name_requires_manage_perm(api_client: APIClient): head = make_user("head@example.com") d = Department.objects.create(name="Dept", head=head) @@ -282,7 +266,6 @@ def test_partial_update_name_requires_manage_perm(api_client: APIClient): d.refresh_from_db() assert d.name == "New Name" - def test_partial_update_head_requires_change_head_perm(api_client: APIClient): a = make_user("a@example.com") # текущий head b = make_user("b@example.com") # кандидат @@ -315,7 +298,6 @@ def test_partial_update_head_requires_change_head_perm(api_client: APIClient): d.refresh_from_db() assert d.head_id == b.id - def test_partial_update_head_same_value_does_not_require_extra_perm( api_client: APIClient, ): @@ -337,10 +319,8 @@ def test_partial_update_head_same_value_does_not_require_extra_perm( d.refresh_from_db() assert d.head_id == a.id - # ---------- tests: action set_head ---------- - def test_set_head_by_role_with_perm(api_client: APIClient): head = make_user("head@example.com") d = Department.objects.create(name="Dept", head=head) @@ -359,7 +339,6 @@ def test_set_head_by_role_with_perm(api_client: APIClient): d.refresh_from_db() assert d.head_id == candidate.id - def test_set_head_validation_inactive_employee(api_client: APIClient): head = make_user("head@example.com") d = Department.objects.create(name="Dept", head=head) @@ -380,7 +359,6 @@ def test_set_head_validation_inactive_employee(api_client: APIClient): == status.HTTP_400_BAD_REQUEST ) - def test_set_head_remove_with_null(api_client: APIClient): head = make_user("head@example.com") d = Department.objects.create(name="Dept", head=head) @@ -400,7 +378,6 @@ def test_set_head_remove_with_null(api_client: APIClient): d.refresh_from_db() assert d.head_id is None - def test_set_head_requires_perm(api_client: APIClient): head = make_user("head@example.com") d = Department.objects.create(name="Dept", head=head) @@ -418,10 +395,8 @@ def test_set_head_requires_perm(api_client: APIClient): == status.HTTP_403_FORBIDDEN ) - # ---------- tests: action set_member_role ---------- - def test_set_member_role_happy_path(api_client: APIClient): d = Department.objects.create(name="Dept") @@ -446,7 +421,6 @@ def test_set_member_role_happy_path(api_client: APIClient): assert data["role_id"] == r_worker.id assert data["is_active"] is True - def test_set_member_role_reject_foreign_role(api_client: APIClient): d1 = Department.objects.create(name="D1") d2 = Department.objects.create(name="D2") @@ -469,7 +443,6 @@ def test_set_member_role_reject_foreign_role(api_client: APIClient): ) assert resp.status_code == status.HTTP_400_BAD_REQUEST - def test_set_member_role_requires_perm(api_client: APIClient): d = Department.objects.create(name="Dept") manager = make_user("m@example.com") @@ -488,7 +461,6 @@ def test_set_member_role_requires_perm(api_client: APIClient): ) assert resp.status_code == status.HTTP_403_FORBIDDEN - def test_set_member_role_missing_employee_id(api_client: APIClient): d = Department.objects.create(name="Dept") manager = make_user("m@example.com") diff --git a/backend/tests/api/v1/employees/test_email_verification_security.py b/backend/tests/api/v1/employees/test_email_verification_security.py index d2d3dcd3..0b752c95 100644 --- a/backend/tests/api/v1/employees/test_email_verification_security.py +++ b/backend/tests/api/v1/employees/test_email_verification_security.py @@ -15,15 +15,15 @@ from unittest.mock import patch Employee = get_user_model() - @pytest.fixture(autouse=True) def disable_ldap(): """Отключает LDAP для всех тестов.""" with patch("api.v1.employees.views._is_ldap_enabled", return_value=False): yield - +@pytest.fixture def make_user(email, verified=True, **kwargs): + """Fixture для создания пользователей.""" """Создаёт пользователя для тестов.""" phone = kwargs.get( "phone_number", f"+7900{email[:7].replace('@', '').replace('.', '')}" @@ -42,7 +42,6 @@ def make_user(email, verified=True, **kwargs): user.save(update_fields=["email_verified", "is_active"]) return user - @pytest.mark.django_db class TestEmailChangeResetsVerification: """Тесты на сброс email_verified при изменении email.""" @@ -158,7 +157,6 @@ class TestEmailChangeResetsVerification: ), "Верификация не должна сбрасываться для того же email" assert user.email_activation_code == old_code - @pytest.mark.django_db class TestUnverifiedUserRestrictions: """Тесты ограничений для неверифицированных пользователей.""" @@ -270,7 +268,6 @@ class TestUnverifiedUserRestrictions: assert user.email_activation_code != "111111", "Код должен обновиться" assert len(mail.outbox) == 1 - @pytest.mark.django_db class TestResendEmailLogic: """Тесты логики повторной отправки кода.""" @@ -352,7 +349,6 @@ class TestResendEmailLogic: assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.data.get("error") == "invalid_code" - @pytest.mark.django_db class TestStaffCanBypassRestrictions: """Тесты что staff может работать через админку даже без верификации.""" @@ -395,7 +391,6 @@ class TestStaffCanBypassRestrictions: assert superuser.is_active is True assert superuser.is_staff is True - @pytest.mark.django_db class TestEmailVerificationWorkflow: """Тесты полного цикла верификации email.""" diff --git a/backend/tests/api/v1/employees/test_employees.py b/backend/tests/api/v1/employees/test_employees.py index df59bd26..f124e280 100644 --- a/backend/tests/api/v1/employees/test_employees.py +++ b/backend/tests/api/v1/employees/test_employees.py @@ -17,27 +17,25 @@ from employees.models import ( Skill, ) from employees.constants import ACTION_DISMISSED # для фильтра actually_active +from tests.conftest import _unique_phone pytestmark = pytest.mark.django_db User = get_user_model() - # ---------- fixtures / helpers ---------- @pytest.fixture def api_client(): return APIClient() - _phone_seq = itertools.count(2000) - def _unique_phone() -> str: # валидный уникальный E.164 return f"+7999000{next(_phone_seq):03d}" - +@pytest.fixture def make_user( email: str, *, @@ -47,6 +45,7 @@ def make_user( active: bool = True, **extra, ) -> User: + """Fixture для создания пользователей.""" """ Создаём пользователя напрямую (без менеджера, чтобы не слать почту). """ @@ -65,13 +64,11 @@ def make_user( u.save() return u - def extract_results(data): if isinstance(data, dict) and "results" in data: return data["results"] return data - # ---------- basic auth / list / retrieve ---------- def test_list_requires_auth(api_client: APIClient): @@ -79,7 +76,6 @@ def test_list_requires_auth(api_client: APIClient): resp = api_client.get(url) assert resp.status_code == status.HTTP_401_UNAUTHORIZED - def test_list_ok_for_authenticated(api_client: APIClient): user = make_user("u@example.com") e1 = make_user("a@example.com") @@ -94,13 +90,11 @@ def test_list_ok_for_authenticated(api_client: APIClient): ids = {it["id"] for it in items} assert e1.id in ids and e2.id in ids - def test_retrieve_requires_auth(api_client: APIClient): e = make_user("x@example.com") url = reverse("api:v1:employees-detail", args=[e.pk]) assert api_client.get(url).status_code == status.HTTP_401_UNAUTHORIZED - def test_retrieve_ok(api_client: APIClient): user = make_user("u@example.com") e = make_user("x@example.com") @@ -113,7 +107,6 @@ def test_retrieve_ok(api_client: APIClient): assert data["id"] == e.id assert data["email"] == e.email - # ---------- search & ordering ---------- def test_search_by_email_and_last_name(api_client: APIClient): @@ -147,7 +140,6 @@ def test_search_by_email_and_last_name(api_client: APIClient): ids = [x["id"] for x in extract_results(r.json())] assert ids == sorted(ids) - # ---------- filters ---------- def test_filter_by_department_includes_members_and_head(api_client: APIClient): @@ -168,7 +160,6 @@ def test_filter_by_department_includes_members_and_head(api_client: APIClient): assert ids == {head.id, member.id} assert outsider.id not in ids - def test_filter_by_position(api_client: APIClient): u = make_user("u@example.com") api_client.force_authenticate(user=u) @@ -192,7 +183,6 @@ def test_filter_by_position(api_client: APIClient): ids = {it["id"] for it in extract_results(r.json())} assert ids == {e1.id, e2.id} - def test_filter_by_skills_any_of(api_client: APIClient): u = make_user("u@example.com") api_client.force_authenticate(user=u) @@ -214,7 +204,6 @@ def test_filter_by_skills_any_of(api_client: APIClient): ids = {it["id"] for it in extract_results(r.json())} assert ids == {e1.id, e2.id} - def test_filter_by_email_verified_and_active(api_client: APIClient): u = make_user("u@example.com") api_client.force_authenticate(user=u) @@ -233,7 +222,6 @@ def test_filter_by_email_verified_and_active(api_client: APIClient): ids = {it["id"] for it in extract_results(r.json())} assert v1.id in ids and v0.id in ids and a0.id not in ids - def test_filter_actually_active_logic(api_client: APIClient): u = make_user("u@example.com") api_client.force_authenticate(user=u) @@ -263,7 +251,6 @@ def test_filter_actually_active_logic(api_client: APIClient): # среди "не реально активных" должны быть уволенный и/или не верифицированный assert e_d.id in ids or e_unver.id in ids - # ---------- create (staff only) + validation + skills ---------- def test_create_requires_staff(api_client: APIClient): @@ -295,7 +282,6 @@ def test_create_requires_staff(api_client: APIClient): assert data["email"] == "new@example.com" assert Employee.objects.filter(email="new@example.com").exists() - # ---------- update / partial_update (IsSelfOrStaff) ---------- def test_partial_update_self_allowed(api_client: APIClient): @@ -308,7 +294,6 @@ def test_partial_update_self_allowed(api_client: APIClient): e.refresh_from_db() assert e.first_name == "Me" - def test_partial_update_other_forbidden_for_regular_user(api_client: APIClient): owner = make_user("owner@example.com") other = make_user("other@example.com") @@ -318,7 +303,6 @@ def test_partial_update_other_forbidden_for_regular_user(api_client: APIClient): resp = api_client.patch(url, {"first_name": "Hack"}, format="json") assert resp.status_code == status.HTTP_403_FORBIDDEN - def test_partial_update_other_allowed_for_staff(api_client: APIClient): target = make_user("target@example.com") staff = make_user("admin@example.com", staff=True) @@ -330,7 +314,6 @@ def test_partial_update_other_allowed_for_staff(api_client: APIClient): target.refresh_from_db() assert target.first_name == "Ok" - def test_destroy_requires_admin(api_client: APIClient): victim = make_user("victim@example.com") regular = make_user("regular@example.com") @@ -349,7 +332,6 @@ def test_destroy_requires_admin(api_client: APIClient): assert resp.status_code == status.HTTP_204_NO_CONTENT assert not Employee.objects.filter(pk=victim.pk).exists() - # ---------- action: me (GET/PATCH) ---------- def test_me_get_returns_current_user(api_client: APIClient): @@ -361,7 +343,6 @@ def test_me_get_returns_current_user(api_client: APIClient): assert r.status_code == 200 assert r.json()["id"] == me.id - def test_me_patch_updates_profile(api_client: APIClient): me = make_user("me@example.com", telegram="@me") # уже есть контакт api_client.force_authenticate(user=me) @@ -372,7 +353,6 @@ def test_me_patch_updates_profile(api_client: APIClient): me.refresh_from_db() assert me.first_name == "Updated" - def test_me_patch_rejects_when_all_contacts_empty(api_client: APIClient): """ Ваша логика в me(): если пользователь не передал поля контактов и в итоге diff --git a/backend/tests/api/v1/employees/test_employees_fields_in_list.py b/backend/tests/api/v1/employees/test_employees_fields_in_list.py index 01b7b734..6f9c582b 100644 --- a/backend/tests/api/v1/employees/test_employees_fields_in_list.py +++ b/backend/tests/api/v1/employees/test_employees_fields_in_list.py @@ -8,25 +8,20 @@ from employees.models import (Department, DepartmentRole, EmployeeDepartment, Position) from rest_framework import status from rest_framework.test import APIClient +from tests.conftest import _unique_phone pytestmark = pytest.mark.django_db User = get_user_model() # --- локальные хелперы (не импортируем из других тестов) --- - @pytest.fixture def api(): return APIClient() - _phone_seq = itertools.count(3000) - -def _unique_phone() -> str: - return f"+7999000{next(_phone_seq):03d}" - - +@pytest.fixture def make_user( email: str, *, @@ -36,6 +31,7 @@ def make_user( active: bool = True, **extra, ) -> User: + """Fixture для создания пользователей.""" u = User.objects.create( email=email, phone_number=extra.pop("phone_number", _unique_phone()), @@ -51,23 +47,19 @@ def make_user( u.save() return u - def extract_results(data): if isinstance(data, dict) and "results" in data: return data["results"] return data - def _get_item(items, emp_id): for it in items: if it.get("id") == emp_id: return it return None - # --- тесты полей списка --- - def test_list_includes_basic_fields(api): """ В элементе списка должны быть: @@ -105,7 +97,6 @@ def test_list_includes_basic_fields(api): "Ожидали 'position' (объект) или 'position_id' в списке сотрудников" ) - def test_list_departments_include_role(api): """ У сотрудника в списке есть departments[], и для отдела с назначенной diff --git a/backend/tests/api/v1/employees/test_ldap_optional_groups.py b/backend/tests/api/v1/employees/test_ldap_optional_groups.py index e899f867..47040348 100644 --- a/backend/tests/api/v1/employees/test_ldap_optional_groups.py +++ b/backend/tests/api/v1/employees/test_ldap_optional_groups.py @@ -12,18 +12,16 @@ from django.contrib.auth.models import Group from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient +from tests.conftest import _unique_phone User = get_user_model() pytestmark = pytest.mark.django_db _phone_seq = itertools.count(5000) - -def _unique_phone(): - return f"+7999000{next(_phone_seq):04d}" - - +@pytest.fixture def make_user(email, **kwargs): + """Fixture для создания пользователей.""" """Создаёт пользователя напрямую""" u = User.objects.create( email=email, @@ -40,20 +38,16 @@ def make_user(email, **kwargs): u.save() return u - @pytest.fixture def api_client(): return APIClient() - @pytest.fixture def admin_user(): return make_user("admin@test.com", staff=True, superuser=True) - # ---------- Создание группы ---------- - @pytest.mark.skip(reason="Requires real LDAP connection - skipped for safety") @patch("api.v1.employees.views.DirectoryService") def test_create_group_with_ldap( @@ -81,7 +75,6 @@ def test_create_group_with_ldap( # Проверяем вызов LDAP mock_service.group_create.assert_called_once() - def test_create_group_without_ldap( api_client, admin_user, settings ): @@ -100,10 +93,8 @@ def test_create_group_without_ldap( group = Group.objects.get(name="TestGroup") assert group is not None - # ---------- Добавление участников ---------- - @pytest.mark.skip(reason="Requires real LDAP connection - skipped for safety") @patch("api.v1.employees.views.DirectoryService") def test_add_members_with_ldap( @@ -142,7 +133,6 @@ def test_add_members_with_ldap( # Проверяем вызов LDAP mock_service.group_add_members.assert_called_once() - def test_add_members_without_ldap( api_client, admin_user, settings ): @@ -167,10 +157,8 @@ def test_add_members_without_ldap( # Проверяем добавление в БД assert group.user_set.count() == 2 - # ---------- Удаление группы ---------- - @pytest.mark.skip(reason="Requires real LDAP connection - skipped for safety") @patch("api.v1.employees.views.DirectoryService") def test_destroy_group_with_ldap( @@ -200,7 +188,6 @@ def test_destroy_group_with_ldap( # Проверяем вызов LDAP mock_service.group_delete.assert_called_once() - def test_destroy_group_without_ldap( api_client, admin_user, settings ): @@ -219,10 +206,8 @@ def test_destroy_group_without_ldap( # Проверяем удаление из БД assert not Group.objects.filter(id=group.id).exists() - # ---------- Получение участников ---------- - @pytest.mark.skip(reason="Requires real LDAP connection - skipped for safety") @patch("api.v1.employees.views.DirectoryService") def test_get_members_with_ldap( @@ -253,7 +238,6 @@ def test_get_members_with_ldap( assert len(response.data["dns"]) == 1 assert len(response.data["employees"]) == 1 - def test_get_members_without_ldap( api_client, admin_user, settings ): diff --git a/backend/tests/api/v1/employees/test_ldap_optional_register.py b/backend/tests/api/v1/employees/test_ldap_optional_register.py index e25a8693..a9476053 100644 --- a/backend/tests/api/v1/employees/test_ldap_optional_register.py +++ b/backend/tests/api/v1/employees/test_ldap_optional_register.py @@ -13,22 +13,17 @@ from rest_framework import status from rest_framework.test import APIClient from employees.models import Employee +from tests.conftest import _unique_phone User = get_user_model() pytestmark = pytest.mark.django_db _phone_seq = itertools.count(4000) - -def _unique_phone(): - return f"+7999000{next(_phone_seq):04d}" - - @pytest.fixture def api_client(): return APIClient() - @pytest.fixture def test_user_data(): """Базовые данные для регистрации""" @@ -42,10 +37,8 @@ def test_user_data(): "birth_date": "1990-01-01", } - # ---------- Тесты С LDAP (LDAP_ENABLED=True) ---------- - @pytest.mark.skip(reason="Requires real LDAP connection - skipped for safety") @patch("api.v1.employees.views.DirectoryService") def test_register_with_ldap_creates_user_in_ldap_and_db( @@ -86,7 +79,6 @@ def test_register_with_ldap_creates_user_in_ldap_and_db( assert call_args.email == test_user_data["email"] assert call_args.password == test_user_data["password"] - @pytest.mark.skip(reason="Requires real LDAP connection - skipped for safety") @patch("api.v1.employees.views.DirectoryService") def test_register_with_ldap_duplicate_email_returns_400( @@ -109,10 +101,8 @@ def test_register_with_ldap_duplicate_email_returns_400( assert response.status_code == status.HTTP_400_BAD_REQUEST assert "email" in str(response.data).lower() or "exists" in str(response.data).lower() - # ---------- Тесты БЕЗ LDAP (LDAP_ENABLED=False) ---------- - def test_register_without_ldap_creates_user_only_in_db( api_client, test_user_data, settings ): @@ -136,7 +126,6 @@ def test_register_without_ldap_creates_user_only_in_db( assert user.has_usable_password() # Пароль в БД assert user.check_password(test_user_data["password"]) - def test_register_without_ldap_duplicate_email_returns_400( api_client, test_user_data, settings ): @@ -157,10 +146,8 @@ def test_register_without_ldap_duplicate_email_returns_400( assert response.status_code == status.HTTP_400_BAD_REQUEST - # ---------- Параметризованные тесты (оба режима) ---------- - @pytest.mark.skip(reason="Requires LDAP mocking improvements") @pytest.mark.parametrize( "ldap_enabled", [True, False], ids=["with_ldap", "without_ldap"] diff --git a/backend/tests/api/v1/employees/test_positions.py b/backend/tests/api/v1/employees/test_positions.py index c2c4cdb8..5054b7d7 100644 --- a/backend/tests/api/v1/employees/test_positions.py +++ b/backend/tests/api/v1/employees/test_positions.py @@ -7,16 +7,10 @@ from django.contrib.auth.models import Group, Permission from django.urls import reverse from employees.models import Employee, Position from rest_framework import status +from tests.conftest import _unique_phone # ---------- helpers ---------- -_phone_seq = count(1) - - -def _unique_phone() -> str: - return f"+7999{next(_phone_seq):07d}" - - def _grant_perm(user, code: str): app_label, codename = code.split(".") perm = Permission.objects.get(content_type__app_label=app_label, codename=codename) diff --git a/backend/tests/api/v1/employees/test_role_assignment.py b/backend/tests/api/v1/employees/test_role_assignment.py index f2c25211..b518e93c 100644 --- a/backend/tests/api/v1/employees/test_role_assignment.py +++ b/backend/tests/api/v1/employees/test_role_assignment.py @@ -10,18 +10,13 @@ from employees.models import ( Department, DepartmentPermission, DepartmentRole, EmployeeDepartment, RoleAssignment, ) +from tests.conftest import _unique_phone User = get_user_model() # В этом проекте User == Employee (кастомная модель) Employee = User - -def _unique_phone() -> str: - """Генерирует уникальный номер телефона.""" - base = 79000000000 - return str(base + User.objects.count()) - - +@pytest.fixture def make_user(email: str, is_staff: bool = False) -> User: """Создаёт пользователя (Employee).""" return User.objects.create_user( @@ -32,12 +27,10 @@ def make_user(email: str, is_staff: bool = False) -> User: send_activation_email=False, ) - def make_dept(name: str, head: User = None) -> Department: """Создаёт отдел.""" return Department.objects.create(name=name, head=head) - def make_role(dept: Department, name: str, perm_codes: list[str]) -> DepartmentRole: """Создаёт роль с указанными правами.""" role = DepartmentRole.objects.create(department=dept, name=name) @@ -48,7 +41,6 @@ def make_role(dept: Department, name: str, perm_codes: list[str]) -> DepartmentR role.scoped_permissions.add(perm) return role - def grant_assign_perm(user: User, dept: Department) -> DepartmentRole: """Даёт пользователю право assign_department_role в отделе.""" role = make_role(dept, "Assigner", ["assign_department_role"]) @@ -56,12 +48,10 @@ def grant_assign_perm(user: User, dept: Department) -> DepartmentRole: RoleAssignment.objects.create(employee=user, role=role, is_active=True) return role - @pytest.fixture def api_client() -> APIClient: return APIClient() - @pytest.mark.django_db class TestRoleAssignmentEndpoints: """Тесты для /department-roles/{id}/assign/, revoke, assignments.""" @@ -180,7 +170,6 @@ class TestRoleAssignmentEndpoints: assert resp.status_code == status.HTTP_201_CREATED - @pytest.mark.django_db class TestRoleAssignmentPermissions: """Тесты для проверки прав через RoleAssignment.""" diff --git a/backend/tests/api/v1/employees/test_skills.py b/backend/tests/api/v1/employees/test_skills.py index 6586e4b4..ad8e9d5b 100644 --- a/backend/tests/api/v1/employees/test_skills.py +++ b/backend/tests/api/v1/employees/test_skills.py @@ -3,29 +3,16 @@ from django.contrib.auth.models import Permission from django.urls import reverse from employees.models import Employee, Skill from rest_framework import status +from tests.conftest import _unique_email, _unique_phone +from tests.test_config import DEFAULT_PASSWORD # --- helpers --- -_seq = 1 - - -def _uniq_email(prefix="u"): - global _seq - _seq += 1 - return f"{prefix}{_seq}@example.com" - - -def _uniq_phone(): - global _seq - _seq += 1 - return f"+7999{_seq:07d}" - - def _mk_user(staff=False, superuser=False) -> Employee: u = Employee.objects.create_user( - email=_uniq_email(), - password="pass", - phone_number=_uniq_phone(), + email=_unique_email(), + password=DEFAULT_PASSWORD, + phone_number=_unique_phone(), send_activation_email=False, first_name="T", last_name="U", diff --git a/backend/tests/api/v1/feed/test_comments.py b/backend/tests/api/v1/feed/test_comments.py index 3be84bb0..02c1109d 100644 --- a/backend/tests/api/v1/feed/test_comments.py +++ b/backend/tests/api/v1/feed/test_comments.py @@ -7,25 +7,15 @@ from rest_framework import status from employees.models import Employee from feed.models import Post, Comment from feed.constants import TYPE_EMPLOYEE, TYPE_COMPANY +from tests.conftest import _unique_email, _unique_phone # ---------- helpers ---------- -_seq = 1 -def _uniq_email(prefix="u"): - global _seq - _seq += 1 - return f"{prefix}{_seq}@example.com" - -def _uniq_phone(): - global _seq - _seq += 1 - return f"+7999{_seq:07d}" - def _user(staff=False, superuser=False) -> Employee: u = Employee.objects.create_user( - email=_uniq_email(), password="pass", - phone_number=_uniq_phone(), + email=_unique_email(), password="pass", + phone_number=_unique_phone(), send_activation_email=False, first_name="T", last_name="U", ) if staff: diff --git a/backend/tests/api/v1/feed/test_posts.py b/backend/tests/api/v1/feed/test_posts.py index 3303c504..392703aa 100644 --- a/backend/tests/api/v1/feed/test_posts.py +++ b/backend/tests/api/v1/feed/test_posts.py @@ -30,10 +30,8 @@ User = get_user_model() pytestmark = pytest.mark.django_db - # ------------------------ helpers ------------------------ - def _ensure_permission( app_label: str, model: type, codename: str, name: Optional[str] = None ) -> Permission: @@ -59,7 +57,6 @@ def _ensure_permission( ) return perm - def _mk_dept(name: str = "Dept", *, head: Optional[User] = None) -> Department: """Создаёт отдел. @@ -72,7 +69,6 @@ def _mk_dept(name: str = "Dept", *, head: Optional[User] = None) -> Department: """ return Department.objects.create(name=name, head=head) - def _ensure_dept_permission(code: str, name: str | None = None) -> DepartmentPermission: """Возвращает/создаёт DepartmentPermission с заданным кодом.""" perm, _ = DepartmentPermission.objects.get_or_create( @@ -81,7 +77,6 @@ def _ensure_dept_permission(code: str, name: str | None = None) -> DepartmentPer ) return perm - def _give_dept_role_with_perms(user, dept, *, perm_codenames, role_name="Role"): """Создаёт роль отдела, вешает scoped_permissions и назначает пользователю.""" role = DepartmentRole.objects.create(name=role_name, department=dept) @@ -95,7 +90,6 @@ def _give_dept_role_with_perms(user, dept, *, perm_codenames, role_name="Role"): ) return role - def _post_data( *, type_: str, title: str = "T", body: str = "B", dept: Optional[Department] = None ) -> dict: @@ -115,7 +109,6 @@ def _post_data( data["department"] = dept.id if dept else None return data - def _create_post( author: User, *, type_: str, dept: Optional[Department] = None, title: str = "T" ) -> Post: @@ -134,10 +127,8 @@ def _create_post( author=author, type=type_, department=dept, title=title, body="B" ) - # ------------------------ URL helpers ------------------------ - @pytest.fixture def urls(): """Именованные URL-роуты фида. @@ -156,23 +147,8 @@ def urls(): "comment_detail": lambda pk: reverse("api:v1:comments-detail", args=[pk]), } - # ------------------------ fixtures (project-aware) ------------------------ - - -@pytest.fixture -def make_user(user_factory): - """Фабрика пользователей на основе проектной фикстуры. - - Returns: - Callable[..., User]: Создатель пользователя. - """ - - def _mk(email: str, **extra) -> User: - return user_factory(email=email, **extra) - - return _mk - +# make_user импортируется из tests.conftest @pytest.fixture def auth(): @@ -184,7 +160,6 @@ def auth(): # ------------------------ A. Аноним ------------------------ - class TestAnonymousAccess: """A. Аноним: все эндпоинты → 401.""" @@ -219,10 +194,8 @@ class TestAnonymousAccess: == 401 ) - # ------------------------ B. Обычный пользователь ------------------------ - class TestRegularUser: """B. Обычный аутентифицированный пользователь: чтение/коммент/лайки 2xx, посты отдела 403.""" @@ -266,10 +239,8 @@ class TestRegularUser: assert client.post(urls["post_pin"](post.id)).status_code == 403 assert client.post(urls["post_unpin"](post.id)).status_code == 403 - # ------------------------ C. Роль в отделе ------------------------ - class TestDepartmentRole: """C. Роль в отделе с perm 'manage_department_feed' даёт запись в своём отделе, но не в чужом.""" @@ -314,10 +285,8 @@ class TestDepartmentRole: ) assert r2.status_code == 403 - # ------------------------ D. Руководитель отдела ------------------------ - class TestDepartmentHead: """D. Head: полные права на посты своего отдела.""" @@ -343,10 +312,8 @@ class TestDepartmentHead: ) assert client.delete(urls["post_detail"](pid)).status_code in (200, 204) - # ------------------------ E. staff/superuser и pin ------------------------ - class TestStaffAndPinning: """E. Staff/superuser: полный доступ; pin/unpin доступны только staff.""" @@ -385,10 +352,8 @@ class TestStaffAndPinning: ) assert client.delete(urls["post_detail"](pid2)).status_code in (200, 204) - # ------------------------ F. Глобальные модельные права на company ------------------------ - class TestCompanyModelPermsGranular: """Гранулярные проверки модельных прав для company-постов.""" @@ -508,10 +473,8 @@ class TestCompanyModelPermsGranular: == 403 ) - # ------------------------ G. Комментарии ------------------------ - class TestComments: """G. Комментарии: создание любым аутентифицированным; изменение — автор/staff.""" @@ -555,10 +518,8 @@ class TestComments: get_after_del = client_r.get(urls["comment_detail"](cid)) assert get_after_del.status_code == 404 - # ------------------------ H. Лайки (идемпотентность и is_liked) ------------------------ - class TestLikes: """H. Лайки: идемпотентность и синхронизация is_liked.""" @@ -598,10 +559,8 @@ class TestLikes: d2 = client.get(urls["post_detail"](post.id)).json() assert d2.get("is_liked") in (False, None) - # ------------------------ I. Негативные кейсы валидации ------------------------ - class TestValidation: """I. Негативные кейсы: отсутствие department, запрет чужого отдела и т.п.""" @@ -630,7 +589,6 @@ class TestValidation: ) assert r.status_code == 403 - def test_company_create_forbidden_for_regular_user(make_user, auth, urls): u = make_user("regular@example.com") client = auth(u) diff --git a/backend/tests/api/v1/requests_app/conftest.py b/backend/tests/api/v1/requests_app/conftest.py index fa257175..4a765eb0 100644 --- a/backend/tests/api/v1/requests_app/conftest.py +++ b/backend/tests/api/v1/requests_app/conftest.py @@ -17,7 +17,6 @@ from .contract import MODEL_VIEW_PERMISSION _phone_counter: Final = itertools.count(100_000_000) # 9 цифр для номера после +79 _email_counter: Final = itertools.count(1) # для уникальных email по умолчанию - def _detect_phone_field(User: type[models.Model]) -> Optional[str]: """Возвращает имя телефонного поля модели пользователя, если оно есть. @@ -39,7 +38,6 @@ def _detect_phone_field(User: type[models.Model]) -> Optional[str]: continue return None - def _next_ru_phone() -> str: """Генерирует уникальный российский номер в формате E.164 (+79XXXXXXXXX). @@ -49,7 +47,6 @@ def _next_ru_phone() -> str: # примечание: переполнение счётчика в тестовой среде нереалистично return f"+79{next(_phone_counter):09d}" - @pytest.fixture def api_client() -> APIClient: """Чистый DRF-клиент без авторизации. @@ -59,7 +56,6 @@ def api_client() -> APIClient: """ return APIClient() - @pytest.fixture def make_user() -> Callable[..., models.Model]: """Фабрика пользователей, совместимая с кастомной моделью (телефон может быть обязателен). @@ -128,19 +124,16 @@ def make_user() -> Callable[..., models.Model]: return _make_user - @pytest.fixture def regular_user(make_user: Callable[..., models.Model]) -> models.Model: """Обычный пользователь без спецправ.""" return make_user(email="regular@example.com") - @pytest.fixture def admin_user(make_user: Callable[..., models.Model]) -> models.Model: """Администратор (суперпользователь).""" return make_user(email="admin@example.com", is_staff=True, is_superuser=True) - @pytest.fixture def auth_client() -> Callable[[models.Model], APIClient]: """Фабрика авторизованных DRF-клиентов с использованием force_authenticate. @@ -157,7 +150,6 @@ def auth_client() -> Callable[[models.Model], APIClient]: return _build - @pytest.fixture def grant_model_perm(db): def _grant(user, perm_str: str): @@ -174,7 +166,6 @@ def grant_model_perm(db): return _grant - @pytest.fixture def make_request(db): """Фабрика: создаёт заявку с минимальными полями. diff --git a/backend/tests/api/v1/requests_app/test_requests_api.py b/backend/tests/api/v1/requests_app/test_requests_api.py index 8ef010ab..65a6a1c2 100644 --- a/backend/tests/api/v1/requests_app/test_requests_api.py +++ b/backend/tests/api/v1/requests_app/test_requests_api.py @@ -11,10 +11,12 @@ from requests_app.models import Request as Req from requests_app.enums import RequestStatus, RequestType from django.contrib.auth import get_user_model +from tests.test_config import API_REQUESTS_URL + pytestmark = pytest.mark.django_db -API_BASE = "/api/v1/requests/" +API_BASE = API_REQUESTS_URL def _results(payload: Any) -> List[Dict[str, Any]]: diff --git a/backend/tests/api/v1/requests_app/test_requests_departments.py b/backend/tests/api/v1/requests_app/test_requests_departments.py index 5f259d9e..7c71e742 100644 --- a/backend/tests/api/v1/requests_app/test_requests_departments.py +++ b/backend/tests/api/v1/requests_app/test_requests_departments.py @@ -7,8 +7,10 @@ import pytest from django.db import transaction from django.utils.crypto import get_random_string -# Константа API — как и в существующих тестах -API_BASE = "/api/v1/requests/" +from tests.test_config import API_REQUESTS_URL + +# Константа API +API_BASE = API_REQUESTS_URL from employees.models import (