Update API tests: improve fixtures and LDAP optional mode support
This commit is contained in:
parent
624784b788
commit
4c398856ae
@ -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]:
|
||||
"""Фабрика событий календаря.
|
||||
|
||||
@ -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."""
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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):
|
||||
"""
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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(): если пользователь не передал поля контактов и в итоге
|
||||
|
||||
@ -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[], и для отдела с назначенной
|
||||
|
||||
@ -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
|
||||
):
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
"""Фабрика: создаёт заявку с минимальными полями.
|
||||
|
||||
@ -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]]:
|
||||
|
||||
@ -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 (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user