chore: закоммичен весь прогресс по тестам и фиксам requests API

This commit is contained in:
admin i 2025-12-03 07:21:25 +03:00
parent 4dee5c8f11
commit 766f9fa919
6 changed files with 1647 additions and 0 deletions

View File

@ -0,0 +1,366 @@
# План тестирования API заявлений (Requests)
## 🎯 Цель
Полное покрытие тестами логики доступа к заявлениям с учетом:
- Модельных прав (permissions)
- Департаментных прав (scoped permissions)
- Получателей (recipients/cc_users)
- Отделов (departments)
- Флага sent_to_all_department
---
## 📋 Матрица тестовых сценариев
### 1. Тесты доступа на чтение (GET /api/v1/requests/)
#### 1.1 Обычные пользователи (без прав)
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 1.1.1 | Получить список всех заявок | Видит только: свои + адресованные ему |
| 1.1.2 | Получить свою заявку (retrieve) | ✅ Доступ разрешен |
| 1.1.3 | Получить чужую заявку (не адресованную) | ❌ 403/404 Forbidden |
| 1.1.4 | Получить заявку, где он в recipients | ✅ Доступ разрешен |
| 1.1.5 | Получить заявку, где он в cc_users | ✅ Доступ разрешен |
| 1.1.6 | Получить заявку с sent_to_all_department для его отдела | ✅ Доступ разрешен |
| 1.1.7 | Получить заявку с sent_to_all_department для чужого отдела | ❌ Не видит |
| 1.1.8 | Фильтр ?mine=true | Видит только свои заявки |
| 1.1.9 | Фильтр ?addressed_to_me=true | Видит только адресованные ему |
#### 1.2 Руководители отделов (department.head)
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 1.2.1 | Получить список заявок | Видит: свои + своего отдела + адресованные |
| 1.2.2 | Получить заявку сотрудника своего отдела | ✅ Доступ разрешен |
| 1.2.3 | Получить заявку сотрудника чужого отдела | ❌ Не видит (если не адресована) |
| 1.2.4 | Получить заявку, адресованную его отделу | ✅ Доступ разрешен |
| 1.2.5 | Получить заявку с sent_to_all_department для его отдела | ✅ Доступ разрешен |
#### 1.3 Пользователи с департаментным правом view_request
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 1.3.1 | Получить список заявок | Видит: свои + отделов с правом + адресованные |
| 1.3.2 | Получить заявку сотрудника отдела (где есть право) | ✅ Доступ разрешен |
| 1.3.3 | Получить заявку сотрудника другого отдела | ❌ Не видит |
| 1.3.4 | Получить заявку, адресованную отделу (где есть право) | ✅ Доступ разрешен |
#### 1.4 Пользователи с департаментным правом can_process_requests
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 1.4.1 | Получить список заявок | Видит: свои + отделов с правом + адресованные |
| 1.4.2 | Получить заявку для согласования | ✅ Доступ разрешен |
| 1.4.3 | Получить заявку чужого отдела | ❌ Не видит |
#### 1.5 Пользователи с глобальным правом can_view_all_requests
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 1.5.1 | Получить список всех заявок | ✅ Видит ВСЕ заявки |
| 1.5.2 | Получить любую заявку (retrieve) | ✅ Доступ разрешен |
| 1.5.3 | Фильтр ?mine=true | Видит только свои |
| 1.5.4 | Фильтр ?addressed_to_me=true | Видит только адресованные |
#### 1.6 Django staff пользователи
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 1.6.1 | Получить список всех заявок | ✅ Видит ВСЕ заявки |
| 1.6.2 | Получить любую заявку | ✅ Доступ разрешен |
---
### 2. Тесты создания (POST /api/v1/requests/)
#### 2.1 Обычные пользователи
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 2.1.1 | Создать заявку без получателей | ✅ Успешно (backward compatibility) |
| 2.1.2 | Создать заявку с recipients | ✅ Успешно |
| 2.1.3 | Создать заявку с cc_users | ✅ Успешно |
| 2.1.4 | Создать заявку с departments | ✅ Успешно |
| 2.1.5 | Создать заявку с sent_to_all_department | ✅ Успешно |
| 2.1.6 | Создать заявку: recipients + cc + departments | ✅ Успешно (комбинация) |
| 2.1.7 | Создать заявку с несуществующим recipient_id | ❌ 400 Validation error |
| 2.1.8 | Создать заявку с несуществующим department_id | ❌ 400 Validation error |
| 2.1.9 | Создать заявку: себя в recipients | ✅ Успешно (валидация не запрещает) |
| 2.1.10 | Создать заявку: дубли в recipients | ✅ ManyToMany устранит дубли |
| 2.1.11 | Создать заявку: recipient_ids как JSON array | ✅ Успешно |
| 2.1.12 | Создать заявку: recipient_ids как CSV | ✅ Успешно |
| 2.1.13 | Создать заявку: recipient_ids как repeat params | ✅ Успешно |
#### 2.2 Уведомления при создании
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 2.2.1 | Создать заявку с recipients | Уведомления отправлены recipients |
| 2.2.2 | Создать заявку с cc_users | Уведомления отправлены cc_users |
| 2.2.3 | Создать заявку с departments | Уведомления руководителям отделов |
| 2.2.4 | Создать с sent_to_all_department | Уведомления ВСЕМ в отделах |
| 2.2.5 | Проверить metadata уведомлений | is_primary_recipient, is_cc корректны |
---
### 3. Тесты обновления (PUT/PATCH /api/v1/requests/{id}/)
#### 3.1 Автор заявки
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 3.1.1 | Обновить свою заявку (не финальный статус) | ✅ Успешно |
| 3.1.2 | Обновить recipients своей заявки | ✅ Успешно |
| 3.1.3 | Обновить cc_users своей заявки | ✅ Успешно |
| 3.1.4 | Обновить departments своей заявки | ✅ Успешно |
| 3.1.5 | Обновить свою заявку (финальный статус) | ❌ 403 Forbidden |
| 3.1.6 | Обновить чужую заявку | ❌ 403 Forbidden |
#### 3.2 Staff / с правами
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 3.2.1 | Staff обновляет любую заявку | ✅ Успешно |
| 3.2.2 | С change_request обновляет заявку | ✅ Успешно |
---
### 4. Тесты согласования (approve/reject)
#### 4.1 Пользователи с can_process_requests (департаментное)
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 4.1.1 | Approve заявку своего отдела | ✅ Успешно |
| 4.1.2 | Approve заявку чужого отдела | ❌ 403 Forbidden |
| 4.1.3 | Reject заявку своего отдела | ✅ Успешно |
| 4.1.4 | Approve уже approved заявку | ❌ 400 Bad request |
#### 4.2 Пользователи с can_process_requests (глобальное)
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 4.2.1 | Approve любую заявку | ✅ Успешно |
| 4.2.2 | Reject любую заявку | ✅ Успешно |
#### 4.3 Уведомления при согласовании
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 4.3.1 | Approve заявку | Уведомление автору + recipients + cc |
| 4.3.2 | Reject заявку | Уведомление автору + recipients + cc |
| 4.3.3 | Проверить metadata | is_approver=True для согласующего |
---
### 5. Тесты отмены (cancel)
#### 5.1 Автор заявки
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 5.1.1 | Cancel свою заявку (не финальный статус) | ✅ Успешно |
| 5.1.2 | Cancel свою заявку (уже approved) | ❌ 403/400 Forbidden |
| 5.1.3 | Cancel чужую заявку | ❌ 403 Forbidden |
#### 5.2 Staff
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 5.2.1 | Cancel любую заявку | ✅ Успешно |
---
### 6. Тесты удаления (DELETE)
#### 6.1 Автор заявки
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 6.1.1 | Delete свою заявку (не финальный статус) | ✅ Успешно |
| 6.1.2 | Delete свою заявку (финальный статус) | ❌ 403 Forbidden |
| 6.1.3 | Delete чужую заявку | ❌ 403 Forbidden |
#### 6.2 Staff / с правами
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 6.2.1 | Staff удаляет любую заявку | ✅ Успешно |
| 6.2.2 | С delete_request удаляет заявку | ✅ Успешно |
---
### 7. Тесты комментариев (GET/POST /api/v1/requests/{id}/comments/)
#### 7.1 Чтение комментариев
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 7.1.1 | Автор читает комментарии к своей заявке | ✅ Успешно |
| 7.1.2 | Получатель читает комментарии | ✅ Успешно |
| 7.1.3 | CC читает комментарии | ✅ Успешно |
| 7.1.4 | Посторонний читает комментарии | ❌ 403 Forbidden |
| 7.1.5 | Staff читает любые комментарии | ✅ Успешно |
#### 7.2 Создание комментариев
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 7.2.1 | Автор комментирует свою заявку | ✅ Успешно |
| 7.2.2 | Получатель комментирует заявку | ✅ Успешно |
| 7.2.3 | CC комментирует заявку | ✅ Успешно |
| 7.2.4 | Посторонний комментирует заявку | ❌ 403 Forbidden |
| 7.2.5 | Staff комментирует любую заявку | ✅ Успешно |
#### 7.3 Уведомления при комментариях
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 7.3.1 | Добавить комментарий | Уведомления: автор + recipients + cc + approver |
| 7.3.2 | Автор комментирует | Уведомления только recipients/cc (не себе) |
| 7.3.3 | Проверить metadata | Корректные данные о комментарии |
---
### 8. Тесты фильтрации
#### 8.1 Фильтры queryset
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 8.1.1 | ?type=vacation | Только заявки типа vacation |
| 8.1.2 | ?status=pending | Только pending заявки |
| 8.1.3 | ?mine=true | Только мои заявки |
| 8.1.4 | ?addressed_to_me=true | Только адресованные мне |
| 8.1.5 | ?view=mine | Только мои заявки (альтернатива) |
| 8.1.6 | Комбинация фильтров | Работает корректно |
---
### 9. Тесты валидации
#### 9.1 Валидация полей
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 9.1.1 | recipient_ids: невалидный формат | ❌ 400 Validation error |
| 9.1.2 | recipient_ids: несуществующий ID | ❌ 400 User not found |
| 9.1.3 | department_ids: несуществующий ID | ❌ 400 Department not found |
| 9.1.4 | Vacation без date_from/date_to | ❌ 400 Validation error |
| 9.1.5 | date_from > date_to | ❌ 400 Validation error |
---
### 10. Тесты обратной совместимости
#### 10.1 Старое поле department
| # | Сценарий | Ожидаемый результат |
|---|----------|---------------------|
| 10.1.1 | Создать заявку со старым полем department | ✅ Успешно (backward compatibility) |
| 10.1.2 | Читать заявку со старым полем | ✅ department возвращается |
| 10.1.3 | Заявка с department (старым) видна уполномоченным | ✅ Доступ работает |
---
## 🏗️ Структура тестов
```
tests/
├── conftest.py # Fixtures
├── requests_app/
│ ├── __init__.py
│ ├── test_list_requests.py # GET /api/v1/requests/
│ ├── test_retrieve_request.py # GET /api/v1/requests/{id}/
│ ├── test_create_request.py # POST /api/v1/requests/
│ ├── test_update_request.py # PUT/PATCH
│ ├── test_approve_reject.py # approve/reject actions
│ ├── test_cancel_request.py # cancel action
│ ├── test_delete_request.py # DELETE
│ ├── test_comments.py # comments endpoint
│ ├── test_notifications.py # уведомления
│ ├── test_filters.py # фильтрация
│ ├── test_validation.py # валидация
│ └── test_permissions.py # комплексные тесты прав
```
---
## 🎭 Fixtures (conftest.py)
### Пользователи:
- `regular_user` - обычный сотрудник без прав
- `dept_head` - руководитель отдела
- `dept_viewer` - с правом view_request в отделе
- `dept_processor` - с правом can_process_requests в отделе
- `global_viewer` - с can_view_all_requests
- `staff_user` - Django staff
- `superuser` - superuser
### Данные:
- `department` - тестовый отдел
- `another_department` - второй отдел
- `request_data` - базовые данные для создания заявки
- `api_client` - APIClient с аутентификацией
### Утилиты:
- `create_request_with_recipients` - хелпер для создания заявки
- `assert_notification_sent` - проверка уведомлений
---
## 📊 Метрики покрытия
**Целевые показатели:**
- Coverage: **>90%** для requests_app/
- Coverage API views: **100%**
- Coverage serializers: **100%**
- Coverage permissions: **100%**
**Команда запуска:**
```bash
pytest tests/requests_app/ -v --cov=requests_app --cov-report=html
```
---
## 🚀 Приоритеты реализации
### Этап 1 (высокий приоритет):
- ✅ Тесты доступа на чтение (сценарии 1.1-1.6)
- ✅ Тесты создания заявок (сценарии 2.1)
- ✅ Тесты согласования (сценарии 4.1-4.2)
### Этап 2 (средний приоритет):
- ✅ Тесты обновления (сценарии 3.1-3.2)
- ✅ Тесты комментариев (сценарии 7.1-7.2)
- ✅ Тесты уведомлений (сценарии 2.2, 4.3, 7.3)
### Этап 3 (низкий приоритет):
- ✅ Тесты фильтрации (сценарии 8.1)
- ✅ Тесты валидации (сценарии 9.1)
- ✅ Тесты обратной совместимости (сценарии 10.1)
---
## ✅ Чеклист готовности
- [ ] Созданы все fixtures в conftest.py
- [ ] Реализованы тесты доступа (1.1-1.6)
- [ ] Реализованы тесты создания (2.1-2.2)
- [ ] Реализованы тесты обновления (3.1-3.2)
- [ ] Реализованы тесты согласования (4.1-4.3)
- [ ] Реализованы тесты отмены (5.1-5.2)
- [ ] Реализованы тесты удаления (6.1-6.2)
- [ ] Реализованы тесты комментариев (7.1-7.3)
- [ ] Реализованы тесты фильтрации (8.1)
- [ ] Реализованы тесты валидации (9.1)
- [ ] Реализованы тесты обратной совместимости (10.1)
- [ ] Coverage >90%
- [ ] Все тесты проходят в CI/CD
---
**Итого:** ~150+ тестовых сценариев для полного покрытия функционала! 🎯

View File

@ -0,0 +1,278 @@
"""
Тестирование логики доступа к заявлениям (requests)
Проверяет:
1. Обычные пользователи видят только свои + адресованные им
2. Руководители отделов видят заявки своих отделов
3. Пользователи с глобальными правами видят всё
4. Департаментные права работают корректно
5. sent_to_all_department работает
"""
import os
import sys
import django
# Настройка Django
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
django.setup()
from django.contrib.auth import get_user_model
from django.db.models import Q
from employees.models import Department, EmployeeDepartment
from requests_app.models import Request
User = get_user_model()
def print_section(title):
"""Красивый вывод секции"""
print(f"\n{'=' * 80}")
print(f" {title}")
print('=' * 80)
def test_user_access(user, description=""):
"""Тестирует доступ пользователя к заявлениям"""
print(f"\n👤 Пользователь: {user.get_full_name() or user.username}")
if description:
print(f" {description}")
# Получаем отделы пользователя
my_depts = EmployeeDepartment.objects.filter(
employee=user,
is_active=True
).select_related('department')
print(f"\n📋 Отделы пользователя:")
if my_depts.exists():
for ed in my_depts:
role_name = ed.role.name if ed.role else "Без роли"
print(f" - {ed.department.name} ({role_name})")
if ed.role:
perms = ed.role.scoped_permissions.values_list('code', flat=True)
if perms:
print(f" Права: {', '.join(perms)}")
else:
print(" (нет отделов)")
# Проверяем глобальные права
print(f"\n🔑 Глобальные права:")
print(f" is_staff: {user.is_staff}")
print(f" can_view_all_requests: {user.has_perm('requests_app.can_view_all_requests')}")
print(f" view_request: {user.has_perm('requests_app.view_request')}")
print(f" can_process_requests: {user.has_perm('requests_app.can_process_requests')}")
# Проверяем руководство отделами
head_depts = Department.objects.filter(head=user)
if head_depts.exists():
print(f"\n👔 Руководитель отделов:")
for dept in head_depts:
print(f" - {dept.name}")
# Строим queryset как в реальном API
my_dept_ids = list(my_depts.values_list('department_id', flat=True))
# Проверяем: может ли видеть ВСЁ
can_view_all = (
user.is_staff
or user.has_perm("requests_app.can_view_all_requests")
or user.has_perm("requests_app.view_request")
)
if can_view_all:
print(f"\n✅ Может просматривать ВСЕ заявления (staff или глобальные права)")
qs = Request.objects.all()
else:
# Обычный пользователь - строим scope
scope = Q(employee_id=user.id) # Свои заявки
# Заявки, где я получатель
scope |= Q(recipients=user) | Q(cc_users=user)
# Заявки отделов с sent_to_all_department
if my_dept_ids:
scope |= Q(
sent_to_all_department=True,
departments__in=my_dept_ids
)
# Департаментные права
view_dept_ids = list(
EmployeeDepartment.objects.filter(
employee_id=user.id,
is_active=True,
role__scoped_permissions__code="view_request",
)
.values_list("department_id", flat=True)
.distinct()
)
proc_dept_ids = list(
EmployeeDepartment.objects.filter(
employee_id=user.id,
is_active=True,
role__scoped_permissions__code="can_process_requests",
)
.values_list("department_id", flat=True)
.distinct()
)
head_dept_ids = list(
Department.objects.filter(head_id=user.id).values_list("id", flat=True)
)
combined_ids = set(view_dept_ids) | set(proc_dept_ids) | set(head_dept_ids)
if combined_ids:
print(f"\n📊 Департаментные права для отделов (ID): {combined_ids}")
# Заявки этих отделов
scope |= Q(departments__in=combined_ids)
# Заявки сотрудников этих отделов
dept_emp_ids = list(
EmployeeDepartment.objects.filter(
department_id__in=list(combined_ids),
is_active=True,
)
.values_list("employee_id", flat=True)
.distinct()
)
if dept_emp_ids:
scope |= Q(employee_id__in=dept_emp_ids)
qs = Request.objects.filter(scope).distinct()
# Подсчитываем результаты
total = qs.count()
print(f"\n📈 Доступные заявления: {total}")
if total > 0:
# Разбивка по типам доступа
my_requests = qs.filter(employee=user).count()
addressed_to_me = qs.filter(Q(recipients=user) | Q(cc_users=user)).count()
print(f"\n Детализация:")
print(f" - Мои заявления: {my_requests}")
print(f" - Адресованные мне (To/CC): {addressed_to_me}")
if my_dept_ids:
all_dept = qs.filter(
sent_to_all_department=True,
departments__in=my_dept_ids
).distinct().count()
print(f" - 'Всем в отделе': {all_dept}")
# Последние 5 заявлений для примера
print(f"\n Примеры заявлений (последние 5):")
for req in qs.select_related('employee').order_by('-created_at')[:5]:
author = req.employee.get_full_name() or req.employee.username
# Определяем причину доступа
reasons = []
if req.employee == user:
reasons.append("автор")
if req.recipients.filter(id=user.id).exists():
reasons.append("получатель")
if req.cc_users.filter(id=user.id).exists():
reasons.append("CC")
if req.sent_to_all_department and any(
dept.id in my_dept_ids for dept in req.departments.all()
):
reasons.append("всем в отделе")
reason_str = ", ".join(reasons) if reasons else "департаментные права"
print(f" {req.id:4d}. {req.get_type_display():15s} | {author:20s} | {reason_str}")
return qs
def main():
print_section("ТЕСТИРОВАНИЕ ЛОГИКИ ДОСТУПА К ЗАЯВЛЕНИЯМ")
# Получаем тестовых пользователей
print("\n🔍 Поиск пользователей для тестирования...")
users_to_test = []
# 1. Обычный сотрудник (без особых прав)
regular_user = User.objects.filter(
is_staff=False,
is_superuser=False,
).exclude(
departments_links__role__scoped_permissions__code__in=[
'view_request', 'can_process_requests'
]
).first()
if regular_user:
users_to_test.append((regular_user, "Обычный сотрудник (без особых прав)"))
# 2. Руководитель отдела
head_user = Department.objects.exclude(head__isnull=True).first()
if head_user and head_user.head:
users_to_test.append((head_user.head, f"Руководитель отдела '{head_user.name}'"))
# 3. С департаментными правами
dept_rights_user = EmployeeDepartment.objects.filter(
is_active=True,
role__scoped_permissions__code__in=['view_request', 'can_process_requests']
).select_related('employee').first()
if dept_rights_user:
users_to_test.append((
dept_rights_user.employee,
f"С департаментными правами в отделе '{dept_rights_user.department.name}'"
))
# 4. Staff пользователь
staff_user = User.objects.filter(is_staff=True, is_superuser=False).first()
if staff_user:
users_to_test.append((staff_user, "Django staff (без superuser)"))
# 5. Superuser
superuser = User.objects.filter(is_superuser=True).first()
if superuser:
users_to_test.append((superuser, "Superuser"))
if not users_to_test:
print("\n❌ Не найдено пользователей для тестирования!")
print(" Создайте пользователей и отделы в админке")
return
# Информация о заявлениях в системе
total_requests = Request.objects.count()
print(f"\n📊 Всего заявлений в системе: {total_requests}")
if total_requests == 0:
print("\n⚠️ В системе нет заявлений! Создайте хотя бы одну заявку для тестирования.")
return
# Статистика по получателям
with_recipients = Request.objects.filter(recipients__isnull=False).distinct().count()
with_cc = Request.objects.filter(cc_users__isnull=False).distinct().count()
with_depts = Request.objects.filter(departments__isnull=False).distinct().count()
with_all_dept = Request.objects.filter(sent_to_all_department=True).count()
print(f"\n Заявлений с получателями (To): {with_recipients}")
print(f" Заявлений с копией (CC): {with_cc}")
print(f" Заявлений с отделами: {with_depts}")
print(f" Заявлений 'Всем в отделе': {with_all_dept}")
# Тестируем каждого пользователя
for user, description in users_to_test:
print_section(f"ТЕСТ: {description}")
test_user_access(user, description)
print_section("ТЕСТИРОВАНИЕ ЗАВЕРШЕНО")
print("\n✅ Все проверки выполнены!")
print("\nРекомендации:")
print("1. Проверьте, что обычные пользователи НЕ видят чужие заявки")
print("2. Руководители должны видеть заявки своих отделов")
print("3. Staff видят всё")
print("4. Получатели (To/CC) видят адресованные им заявки")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,289 @@
"""
Детальное тестирование API логики доступа к заявлениям
Проверяет edge cases и специфические сценарии
"""
import os
import sys
import django
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
django.setup()
from django.contrib.auth import get_user_model
from django.db import models
from employees.models import Department, EmployeeDepartment
from requests_app.models import Request
from requests_app.enums import RequestStatus
User = get_user_model()
def test_scenario(title, test_func):
"""Обертка для тестов"""
print(f"\n{'=' * 80}")
print(f"🧪 ТЕСТ: {title}")
print('=' * 80)
try:
result = test_func()
if result:
print(f"✅ PASSED")
else:
print(f"❌ FAILED")
return result
except Exception as e:
print(f"💥 ERROR: {e}")
import traceback
traceback.print_exc()
return False
def test_recipient_can_see():
"""Тест: Получатель (To) видит заявку"""
print("\n1. Создаем пользователя и заявку...")
# Находим пользователя
user1 = User.objects.filter(is_staff=False, is_superuser=False).first()
user2 = User.objects.filter(is_staff=False, is_superuser=False).exclude(id=user1.id).first()
if not user1 or not user2:
print("⚠️ Недостаточно пользователей для теста")
return None
print(f" Автор: {user1.get_full_name() or user1.username}")
print(f" Получатель: {user2.get_full_name() or user2.username}")
# Создаем заявку
request = Request.objects.create(
employee=user1,
type='other',
title='Тест: получатель должен видеть',
status=RequestStatus.PENDING
)
request.recipients.add(user2)
print(f" Создана заявка ID={request.id}")
# Проверяем доступ
print("\n2. Проверяем доступ получателя...")
from django.db.models import Q
my_depts = EmployeeDepartment.objects.filter(
employee=user2, is_active=True
).values_list('department_id', flat=True)
scope = Q(employee_id=user2.id) | Q(recipients=user2) | Q(cc_users=user2)
if my_depts:
scope |= Q(sent_to_all_department=True, departments__in=my_depts)
accessible = Request.objects.filter(scope).filter(id=request.id).exists()
print(f" Получатель видит заявку: {accessible}")
# Cleanup
request.delete()
return accessible
def test_cc_user_can_see():
"""Тест: Пользователь в CC видит заявку"""
print("\n1. Создаем пользователей и заявку с CC...")
user1 = User.objects.filter(is_staff=False, is_superuser=False).first()
user2 = User.objects.filter(is_staff=False, is_superuser=False).exclude(id=user1.id).first()
if not user1 or not user2:
print("⚠️ Недостаточно пользователей для теста")
return None
print(f" Автор: {user1.get_full_name() or user1.username}")
print(f" CC: {user2.get_full_name() or user2.username}")
request = Request.objects.create(
employee=user1,
type='other',
title='Тест: CC должен видеть',
status=RequestStatus.PENDING
)
request.cc_users.add(user2)
print(f" Создана заявка ID={request.id}")
# Проверяем доступ
print("\n2. Проверяем доступ CC пользователя...")
from django.db.models import Q
scope = Q(employee_id=user2.id) | Q(recipients=user2) | Q(cc_users=user2)
accessible = Request.objects.filter(scope).filter(id=request.id).exists()
print(f" CC пользователь видит заявку: {accessible}")
# Cleanup
request.delete()
return accessible
def test_all_department_visibility():
"""Тест: sent_to_all_department работает"""
print("\n1. Создаем заявку 'Всем в отделе'...")
# Находим отдел с сотрудниками
dept = Department.objects.annotate(
emp_count=models.Count('employeedepartment')
).filter(emp_count__gte=2).first()
if not dept:
print("⚠️ Нет отдела с несколькими сотрудниками")
return None
# Берем двух сотрудников этого отдела
emp_dept = EmployeeDepartment.objects.filter(
department=dept,
is_active=True
).select_related('employee')[:2]
if len(emp_dept) < 2:
print("⚠️ Недостаточно сотрудников в отделе")
return None
author = emp_dept[0].employee
viewer = emp_dept[1].employee
print(f" Отдел: {dept.name}")
print(f" Автор: {author.get_full_name() or author.username}")
print(f" Сотрудник отдела: {viewer.get_full_name() or viewer.username}")
# Создаем заявку
request = Request.objects.create(
employee=author,
type='other',
title='Тест: Всем в отделе',
status=RequestStatus.PENDING,
sent_to_all_department=True
)
request.departments.add(dept)
print(f" Создана заявка ID={request.id}")
# Проверяем доступ
print("\n2. Проверяем, что другой сотрудник отдела видит заявку...")
from django.db.models import Q
my_depts = [dept.id]
scope = Q(employee_id=viewer.id) | Q(recipients=viewer) | Q(cc_users=viewer)
scope |= Q(sent_to_all_department=True, departments__in=my_depts)
accessible = Request.objects.filter(scope).filter(id=request.id).exists()
print(f" Сотрудник видит заявку: {accessible}")
# Cleanup
request.delete()
return accessible
def test_non_recipient_cannot_see():
"""Тест: Посторонний НЕ видит заявку"""
print("\n1. Создаем заявку без получателей...")
users = list(User.objects.filter(is_staff=False, is_superuser=False)[:3])
if len(users) < 3:
print("⚠️ Недостаточно пользователей")
return None
author = users[0]
recipient = users[1]
outsider = users[2]
print(f" Автор: {author.get_full_name() or author.username}")
print(f" Получатель: {recipient.get_full_name() or recipient.username}")
print(f" Посторонний: {outsider.get_full_name() or outsider.username}")
request = Request.objects.create(
employee=author,
type='other',
title='Тест: Посторонний не должен видеть',
status=RequestStatus.PENDING
)
request.recipients.add(recipient)
print(f" Создана заявка ID={request.id}")
# Проверяем, что посторонний НЕ видит
print("\n2. Проверяем, что посторонний НЕ видит заявку...")
from django.db.models import Q
my_depts = EmployeeDepartment.objects.filter(
employee=outsider, is_active=True
).values_list('department_id', flat=True)
scope = Q(employee_id=outsider.id) | Q(recipients=outsider) | Q(cc_users=outsider)
if my_depts:
scope |= Q(sent_to_all_department=True, departments__in=my_depts)
accessible = Request.objects.filter(scope).filter(id=request.id).exists()
print(f" Посторонний видит заявку: {accessible}")
print(f" Ожидается: False")
# Cleanup
request.delete()
return not accessible # Тест проходит, если НЕ видит
def main():
print("\n" + "=" * 80)
print(" ДЕТАЛЬНОЕ ТЕСТИРОВАНИЕ API ЛОГИКИ ДОСТУПА")
print("=" * 80)
from django.db import models
results = []
# Тесты
results.append(test_scenario(
"Получатель (To) видит заявку",
test_recipient_can_see
))
results.append(test_scenario(
"Пользователь в CC видит заявку",
test_cc_user_can_see
))
results.append(test_scenario(
"sent_to_all_department работает",
test_all_department_visibility
))
results.append(test_scenario(
"Посторонний НЕ видит чужую заявку",
test_non_recipient_cannot_see
))
# Итоги
print("\n" + "=" * 80)
print(" ИТОГИ ТЕСТИРОВАНИЯ")
print("=" * 80)
passed = sum(1 for r in results if r is True)
failed = sum(1 for r in results if r is False)
skipped = sum(1 for r in results if r is None)
print(f"\n✅ Пройдено: {passed}")
print(f"❌ Провалено: {failed}")
print(f"⏭️ Пропущено: {skipped}")
if failed == 0 and passed > 0:
print("\n🎉 Все тесты пройдены успешно!")
elif failed > 0:
print("\n⚠️ Есть проваленные тесты - требуется исправление!")
if __name__ == "__main__":
main()

View File

@ -72,3 +72,104 @@ def request_obj(user):
status=Request.STATUS_PENDING,
type=Request.TYPE_OTHER,
)
# ============================================================================
# Fixtures для тестирования новых фич (recipients/cc/departments)
# ============================================================================
@pytest.fixture
def test_employee(user_factory):
"""Первый тестовый сотрудник"""
return user_factory(email='emp1@test.com')
@pytest.fixture
def test_employee_second(user_factory):
"""Второй тестовый сотрудник"""
return user_factory(email='emp2@test.com')
@pytest.fixture
def test_employee_third(user_factory):
"""Третий тестовый сотрудник"""
return user_factory(email='emp3@test.com')
@pytest.fixture
def test_department(department_factory):
"""Тестовый отдел"""
return department_factory(name='IT Department')
@pytest.fixture
def test_department_second(department_factory):
"""Второй тестовый отдел"""
return department_factory(name='HR Department')
@pytest.fixture
def test_staff_user(user_factory):
"""Staff пользователь"""
return user_factory(email='staff@test.com', staff=True)
@pytest.fixture
def create_request_helper(db):
"""Хелпер для создания заявки с получателями"""
from requests_app.models import Request
from requests_app.enums import RequestStatus, RequestType
def _create(employee, recipients=None, cc_users=None, departments=None,
sent_to_all=False, **kwargs):
defaults = {
'type': RequestType.OTHER,
'title': 'Test request',
'status': RequestStatus.PENDING
}
defaults.update(kwargs)
request = Request.objects.create(
employee=employee,
sent_to_all_department=sent_to_all,
**defaults
)
if recipients:
if not isinstance(recipients, list):
recipients = [recipients]
request.recipients.set(recipients)
if cc_users:
if not isinstance(cc_users, list):
cc_users = [cc_users]
request.cc_users.set(cc_users)
if departments:
if not isinstance(departments, list):
departments = [departments]
request.departments.set(departments)
return request
return _create
@pytest.fixture
def global_viewer(user_factory):
"""Пользователь с глобальным правом can_view_all_requests"""
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from requests_app.models import Request
user = user_factory(email='global@test.com')
# Получаем или создаем permission
ct = ContentType.objects.get_for_model(Request)
perm, _ = Permission.objects.get_or_create(
content_type=ct,
codename='can_view_all_requests',
defaults={'name': 'Можно просмотреть все заявления'}
)
user.user_permissions.add(perm)
return user

View File

@ -0,0 +1,312 @@
"""
Тесты API: GET /api/v1/requests/ - список заявок
Проверяет логику доступа для различных типов пользователей
"""
import pytest
from requests_app.models import Request
pytestmark = pytest.mark.django_db
class TestListRequestsRegularUser:
"""Тесты для обычных пользователей без прав"""
url = '/api/v1/requests/'
def test_can_see_own_requests(self, auth_client_factory, test_employee):
"""1.1.1: Обычный пользователь видит свои заявки"""
# Создаем заявку
request = Request.objects.create(
employee=test_employee,
type='other',
title='Моя заявка'
)
client = auth_client_factory(test_employee)
response = client.get(self.url)
assert response.status_code == 200
assert response.data['count'] >= 1
ids = [r['id'] for r in response.data['results']]
assert request.id in ids
def test_cannot_see_others_requests(
self, auth_client_factory, test_employee, test_employee_second
):
"""1.1.3: Обычный пользователь НЕ видит чужие заявки"""
# test_employee_second создает заявку
other_request = Request.objects.create(
employee=test_employee_second,
type='other',
title='Чужая заявка'
)
# test_employee пытается получить список
client = auth_client_factory(test_employee)
response = client.get(self.url)
assert response.status_code == 200
ids = [r['id'] for r in response.data['results']]
assert other_request.id not in ids
def test_can_see_request_addressed_to_him(
self, auth_client_factory, test_employee, test_employee_second,
create_request_helper
):
"""1.1.4: Пользователь видит заявку, где он в recipients"""
# test_employee_second создает заявку для test_employee
request = create_request_helper(
employee=test_employee_second,
recipients=[test_employee],
title='Заявка для меня'
)
client = auth_client_factory(test_employee)
response = client.get(self.url)
assert response.status_code == 200
ids = [r['id'] for r in response.data['results']]
assert request.id in ids
def test_can_see_request_cc(
self, auth_client_factory, test_employee, test_employee_second,
create_request_helper
):
"""1.1.5: Пользователь видит заявку, где он в cc_users"""
# test_employee_second создает заявку с test_employee в CC
request = create_request_helper(
employee=test_employee_second,
cc_users=[test_employee],
title='Заявка CC'
)
client = auth_client_factory(test_employee)
response = client.get(self.url)
assert response.status_code == 200
ids = [r['id'] for r in response.data['results']]
assert request.id in ids
def test_can_see_request_all_department(
self, auth_client_factory, test_employee, test_employee_second,
test_department, create_request_helper
):
"""1.1.6: Пользователь видит заявку sent_to_all_department его отдела"""
# Добавляем test_employee в отдел
from employees.models import EmployeeDepartment
EmployeeDepartment.objects.create(
employee=test_employee,
department=test_department,
is_active=True
)
# test_employee_second создает заявку "Всем в отделе"
request = create_request_helper(
employee=test_employee_second,
departments=[test_department],
sent_to_all=True,
title='Всем в IT'
)
client = auth_client_factory(test_employee)
response = client.get(self.url)
assert response.status_code == 200
ids = [r['id'] for r in response.data['results']]
assert request.id in ids
def test_cannot_see_all_department_other_dept(
self, auth_client_factory, test_employee, test_employee_second,
test_department, create_request_helper
):
"""1.1.7: Пользователь НЕ видит sent_to_all_department чужого отдела"""
# test_employee НЕ в test_department
# test_employee_second создает заявку "Всем в отделе"
request = create_request_helper(
employee=test_employee_second,
departments=[test_department],
sent_to_all=True,
title='Всем в IT (но я не в IT)'
)
client = auth_client_factory(test_employee)
response = client.get(self.url)
assert response.status_code == 200
ids = [r['id'] for r in response.data['results']]
assert request.id not in ids
def test_filter_mine(
self, auth_client_factory, test_employee, test_employee_second,
create_request_helper
):
"""1.1.8: Фильтр ?mine=true показывает только свои заявки"""
# Создаем свою заявку
my_request = create_request_helper(
employee=test_employee,
title='Моя заявка'
)
# Создаем заявку, адресованную test_employee
addressed_request = create_request_helper(
employee=test_employee_second,
recipients=[test_employee],
title='Адресованная мне'
)
client = auth_client_factory(test_employee)
response = client.get(self.url, {'mine': 'true'})
assert response.status_code == 200
ids = [r['id'] for r in response.data['results']]
# Должна быть только своя
assert my_request.id in ids
assert addressed_request.id not in ids
def test_filter_addressed_to_me(
self, auth_client_factory, test_employee, test_employee_second,
test_employee_third, create_request_helper
):
"""1.1.9: Фильтр ?addressed_to_me=true показывает только адресованные"""
# Создаем свою заявку
my_request = create_request_helper(
employee=test_employee,
title='Моя заявка'
)
# Создаем заявку, адресованную test_employee
addressed = create_request_helper(
employee=test_employee_second,
recipients=[test_employee],
title='Адресованная мне'
)
# Создаем заявку, адресованную другому
not_addressed = create_request_helper(
employee=test_employee_second,
recipients=[test_employee_third],
title='Адресованная другому'
)
client = auth_client_factory(test_employee)
response = client.get(self.url, {'addressed_to_me': 'true'})
assert response.status_code == 200
ids = [r['id'] for r in response.data['results']]
# Должна быть адресованная, но не своя и не чужая
assert addressed.id in ids
assert my_request.id not in ids
assert not_addressed.id not in ids
class TestListRequestsDeptHead:
"""Тесты для руководителей отделов"""
url = '/api/v1/requests/'
def test_can_see_department_requests(
self, auth_client_factory, test_department, test_employee_second,
create_request_helper, user_factory
):
"""1.2.1: Руководитель видит заявки сотрудников своего отдела"""
# Создаем руководителя с помощью user_factory
from employees.models import EmployeeDepartment
head = user_factory(email='head@test.com')
# Назначаем руководителем
test_department.head = head
test_department.save()
# Добавляем в отдел
EmployeeDepartment.objects.create(
employee=head,
department=test_department,
is_active=True
)
# Добавляем test_employee_second в отдел
EmployeeDepartment.objects.create(
employee=test_employee_second,
department=test_department,
is_active=True
)
# test_employee_second создает заявку
request = create_request_helper(
employee=test_employee_second,
title='Заявка сотрудника'
)
client = auth_client_factory(head)
response = client.get(self.url)
assert response.status_code == 200
ids = [r['id'] for r in response.data['results']]
assert request.id in ids
class TestListRequestsGlobalViewer:
"""Тесты для пользователей с глобальным правом can_view_all_requests"""
url = '/api/v1/requests/'
def test_can_see_all_requests(
self, auth_client_factory, global_viewer, test_employee,
test_employee_second, create_request_helper
):
"""1.5.1: Пользователь с can_view_all_requests видит ВСЕ заявки"""
# Создаем несколько заявок от разных пользователей
req1 = create_request_helper(
employee=test_employee,
title='Заявка 1'
)
req2 = create_request_helper(
employee=test_employee_second,
title='Заявка 2'
)
client = auth_client_factory(global_viewer)
response = client.get(self.url)
assert response.status_code == 200
ids = [r['id'] for r in response.data['results']]
# Должен видеть обе
assert req1.id in ids
assert req2.id in ids
class TestListRequestsStaff:
"""Тесты для Django staff пользователей"""
url = '/api/v1/requests/'
def test_staff_can_see_all_requests(
self, auth_client_factory, test_staff_user, test_employee,
test_employee_second, create_request_helper
):
"""1.6.1: Staff видит ВСЕ заявки"""
# Создаем заявки
req1 = create_request_helper(
employee=test_employee,
title='Заявка 1'
)
req2 = create_request_helper(
employee=test_employee_second,
title='Заявка 2'
)
client = auth_client_factory(test_staff_user)
response = client.get(self.url)
assert response.status_code == 200
ids = [r['id'] for r in response.data['results']]
# Должен видеть обе
assert req1.id in ids
assert req2.id in ids

View File

@ -0,0 +1,301 @@
"""
Pytest fixtures для тестирования requests_app API
"""
import pytest
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission
from employees.models import Department, EmployeeDepartment, DepartmentRole
from requests_app.models import Request
from requests_app.enums import RequestStatus, RequestType
User = get_user_model()
# ============================================================================
# User Fixtures with различными правами
# ============================================================================
@pytest.fixture
def dept_processor(db, test_department):
"""Пользователь с правом can_process_requests в отделе"""
user = User.objects.create_user(
username='dept_processor',
email='processor@test.com',
password='testpass123',
first_name='Согласующий',
last_name='Отдела'
)
# Создаем роль с правом can_process_requests
role = DepartmentRole.objects.create(name='Processor Role')
# Добавляем permission can_process_requests к роли
perm_code = 'can_process_requests'
if hasattr(role, 'permissions_codes'):
role.permissions_codes.append(perm_code)
role.save()
# Добавляем в отдел с ролью
EmployeeDepartment.objects.create(
employee=user,
department=test_department,
role=role,
is_active=True
)
return user
@pytest.fixture
def global_viewer(db):
"""Пользователь с глобальным правом can_view_all_requests"""
user = User.objects.create_user(
username='global_viewer',
email='global@test.com',
password='testpass123',
first_name='Глобальный',
last_name='Просмотрщик'
)
# Пытаемся получить permission, если не найдено - создаем
try:
perm = Permission.objects.get(
content_type__app_label='requests_app',
codename='can_view_all_requests'
)
except Permission.DoesNotExist:
ct = ContentType.objects.get_for_model(Request)
perm = Permission.objects.create(
content_type=ct,
codename='can_view_all_requests',
name='Можно просмотреть все заявления'
)
user.user_permissions.add(perm)
return user
# ============================================================================
# Request Fixtures с различными конфигурациями получателей
# ============================================================================
@pytest.fixture
def request_with_recipients(db, test_employee, test_employee_second):
"""Заявка с получателями"""
request = Request.objects.create(
employee=test_employee,
type=RequestType.OTHER,
title='Заявка с получателями',
status=RequestStatus.PENDING
)
request.recipients.add(test_employee_second)
return request
@pytest.fixture
def request_with_cc(db, test_employee, test_employee_second):
"""Заявка с CC"""
request = Request.objects.create(
employee=test_employee,
type=RequestType.OTHER,
title='Заявка с CC',
status=RequestStatus.PENDING
)
request.cc_users.add(test_employee_second)
return request
@pytest.fixture
def request_to_department(db, test_employee, test_department):
"""Заявка, адресованная отделу"""
request = Request.objects.create(
employee=test_employee,
type=RequestType.OTHER,
title='Заявка для отдела',
status=RequestStatus.PENDING
)
request.departments.add(test_department)
return request
@pytest.fixture
def request_all_department(db, test_employee, test_department):
"""Заявка 'Всем в отделе'"""
request = Request.objects.create(
employee=test_employee,
type=RequestType.OTHER,
title='Всем в отделе',
status=RequestStatus.PENDING,
sent_to_all_department=True
)
request.departments.add(test_department)
return request
@pytest.fixture
def request_complex(db, test_employee, test_employee_second, test_employee_third, test_department):
"""Сложная заявка: recipients + cc + departments"""
request = Request.objects.create(
employee=test_employee,
type=RequestType.OTHER,
title='Комплексная заявка',
status=RequestStatus.PENDING
)
request.recipients.add(test_employee_second)
request.cc_users.add(test_employee_third)
request.departments.add(test_department)
return request
@pytest.fixture
def approved_request(db, test_employee, test_staff_user):
"""Одобренная заявка"""
request = Request.objects.create(
employee=test_employee,
type=RequestType.VACATION,
title='Одобренный отпуск',
status=RequestStatus.APPROVED,
approver=test_staff_user
)
return request
# ============================================================================
# Helper Functions
# ============================================================================
@pytest.fixture
def create_request_helper(db):
"""Хелпер для создания заявки с получателями"""
def _create(employee, recipients=None, cc_users=None, departments=None,
sent_to_all=False, **kwargs):
defaults = {
'type': RequestType.OTHER,
'title': 'Test request',
'status': RequestStatus.PENDING
}
defaults.update(kwargs)
request = Request.objects.create(
employee=employee,
sent_to_all_department=sent_to_all,
**defaults
)
if recipients:
request.recipients.set(recipients if isinstance(recipients, list) else [recipients])
if cc_users:
request.cc_users.set(cc_users if isinstance(cc_users, list) else [cc_users])
if departments:
request.departments.set(departments if isinstance(departments, list) else [departments])
return request
return _create
@pytest.fixture
def check_user_can_see_request():
"""Хелпер для проверки видимости заявки для пользователя"""
def _check(user, request_obj):
from django.db.models import Q
from employees.models import EmployeeDepartment, Department
# Строим queryset как в реальном API
my_dept_ids = list(
EmployeeDepartment.objects.filter(
employee=user,
is_active=True
).values_list('department_id', flat=True)
)
# Проверка: может ли видеть ВСЁ
can_view_all = (
user.is_staff
or user.has_perm("requests_app.can_view_all_requests")
or user.has_perm("requests_app.view_request")
)
if can_view_all:
return True
# Обычный пользователь - строим scope
scope = Q(employee_id=user.id) # Свои заявки
scope |= Q(recipients=user) | Q(cc_users=user) # Адресованные
# Заявки отделов с sent_to_all_department
if my_dept_ids:
scope |= Q(
sent_to_all_department=True,
departments__in=my_dept_ids
)
# Департаментные права
view_dept_ids = list(
EmployeeDepartment.objects.filter(
employee_id=user.id,
is_active=True,
role__scoped_permissions__code="view_request",
)
.values_list("department_id", flat=True)
.distinct()
)
proc_dept_ids = list(
EmployeeDepartment.objects.filter(
employee_id=user.id,
is_active=True,
role__scoped_permissions__code="can_process_requests",
)
.values_list("department_id", flat=True)
.distinct()
)
head_dept_ids = list(
Department.objects.filter(head_id=user.id).values_list("id", flat=True)
)
combined_ids = set(view_dept_ids) | set(proc_dept_ids) | set(head_dept_ids)
if combined_ids:
scope |= Q(departments__in=combined_ids)
dept_emp_ids = list(
EmployeeDepartment.objects.filter(
department_id__in=list(combined_ids),
is_active=True,
)
.values_list("employee_id", flat=True)
.distinct()
)
if dept_emp_ids:
scope |= Q(employee_id__in=dept_emp_ids)
return Request.objects.filter(scope).filter(id=request_obj.id).exists()
return _check
@pytest.fixture
def assert_notification_sent(db):
"""Хелпер для проверки отправки уведомлений"""
def _assert(recipient, notification_type=None, request_obj=None):
from notifications.models import Notification
from django.contrib.contenttypes.models import ContentType
qs = Notification.objects.filter(recipient=recipient)
if notification_type:
qs = qs.filter(notification_type__code=notification_type)
if request_obj:
ct = ContentType.objects.get_for_model(Request)
qs = qs.filter(
content_type=ct,
object_id=request_obj.id
)
assert qs.exists(), (
f"Уведомление не найдено для {recipient.username}"
)
return qs.first()
return _assert