From 7d15ad1a4f8b7e2e1100e66d58fd6e45a4d56c8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Wed, 3 Dec 2025 07:21:25 +0300 Subject: [PATCH] =?UTF-8?q?chore:=20=D0=B7=D0=B0=D0=BA=D0=BE=D0=BC=D0=BC?= =?UTF-8?q?=D0=B8=D1=87=D0=B5=D0=BD=20=D0=B2=D0=B5=D1=81=D1=8C=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B3=D1=80=D0=B5=D1=81=D1=81=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D0=B0=D0=BC=20=D0=B8=20=D1=84=D0=B8?= =?UTF-8?q?=D0=BA=D1=81=D0=B0=D0=BC=20requests=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/REQUESTS_API_TESTING_PLAN.md | 366 ++++++++++++++++++ backend/test_requests_access.py | 278 +++++++++++++ backend/test_requests_access_detailed.py | 289 ++++++++++++++ backend/tests/requests_app/conftest.py | 101 +++++ .../tests/requests_app/test_list_requests.py | 312 +++++++++++++++ backend/tests/requests_app_fixtures.py | 301 ++++++++++++++ 6 files changed, 1647 insertions(+) create mode 100644 backend/REQUESTS_API_TESTING_PLAN.md create mode 100644 backend/test_requests_access.py create mode 100644 backend/test_requests_access_detailed.py create mode 100644 backend/tests/requests_app/test_list_requests.py create mode 100644 backend/tests/requests_app_fixtures.py diff --git a/backend/REQUESTS_API_TESTING_PLAN.md b/backend/REQUESTS_API_TESTING_PLAN.md new file mode 100644 index 00000000..fc24cad4 --- /dev/null +++ b/backend/REQUESTS_API_TESTING_PLAN.md @@ -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+ тестовых сценариев для полного покрытия функционала! 🎯 diff --git a/backend/test_requests_access.py b/backend/test_requests_access.py new file mode 100644 index 00000000..9b807689 --- /dev/null +++ b/backend/test_requests_access.py @@ -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() diff --git a/backend/test_requests_access_detailed.py b/backend/test_requests_access_detailed.py new file mode 100644 index 00000000..9a223cb1 --- /dev/null +++ b/backend/test_requests_access_detailed.py @@ -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() diff --git a/backend/tests/requests_app/conftest.py b/backend/tests/requests_app/conftest.py index b8e27b66..a8b975af 100644 --- a/backend/tests/requests_app/conftest.py +++ b/backend/tests/requests_app/conftest.py @@ -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 + diff --git a/backend/tests/requests_app/test_list_requests.py b/backend/tests/requests_app/test_list_requests.py new file mode 100644 index 00000000..b78741dc --- /dev/null +++ b/backend/tests/requests_app/test_list_requests.py @@ -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 diff --git a/backend/tests/requests_app_fixtures.py b/backend/tests/requests_app_fixtures.py new file mode 100644 index 00000000..a066e436 --- /dev/null +++ b/backend/tests/requests_app_fixtures.py @@ -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