chore: закоммичен весь прогресс по тестам и фиксам requests API
This commit is contained in:
parent
704ebbb7e4
commit
7d15ad1a4f
366
backend/REQUESTS_API_TESTING_PLAN.md
Normal file
366
backend/REQUESTS_API_TESTING_PLAN.md
Normal 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+ тестовых сценариев для полного покрытия функционала! 🎯
|
||||
278
backend/test_requests_access.py
Normal file
278
backend/test_requests_access.py
Normal 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()
|
||||
289
backend/test_requests_access_detailed.py
Normal file
289
backend/test_requests_access_detailed.py
Normal 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()
|
||||
@ -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
|
||||
|
||||
|
||||
312
backend/tests/requests_app/test_list_requests.py
Normal file
312
backend/tests/requests_app/test_list_requests.py
Normal 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
|
||||
301
backend/tests/requests_app_fixtures.py
Normal file
301
backend/tests/requests_app_fixtures.py
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user