Update API tests: improve fixtures and LDAP optional mode support

This commit is contained in:
igor_nadein 2026-01-21 13:13:36 +03:00
parent 624784b788
commit 4c398856ae
20 changed files with 49 additions and 356 deletions

View File

@ -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]:
"""Фабрика событий календаря.

View File

@ -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."""

View File

@ -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")

View File

@ -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):
"""

View File

@ -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")

View File

@ -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")

View File

@ -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")

View File

@ -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."""

View File

@ -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(): если пользователь не передал поля контактов и в итоге

View File

@ -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[], и для отдела с назначенной

View File

@ -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
):

View File

@ -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"]

View File

@ -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)

View File

@ -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."""

View File

@ -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",

View File

@ -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:

View File

@ -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)

View File

@ -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):
"""Фабрика: создаёт заявку с минимальными полями.

View File

@ -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]]:

View File

@ -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 (