feat: add LDAP testing infrastructure
- Add LDAP docker container structure (01-base-structure.ldif) - Add .env.test with LDAP configuration for tests - Create tests/ldap_fixtures.py with LDAP-specific fixtures - Create tests/api/v1/employees/test_helpers.py with centralized helpers - Add LDAP markers to pytest.ini (ldap_required, ldap_optional) - Add comprehensive LDAP testing documentation - Fix test_department_head_rights.py to use centralized helpers New fixtures: - ldap_available: checks if LDAP server is accessible - ldap_config: returns LDAP configuration - ensure_ldap_enabled/disabled: force LDAP state for test - ldap_cleanup: auto-cleanup LDAP entries after test New helpers: - make_user(): centralized user creation - grant_permission(): permission assignment - make_department/position/role(): entity creation - extract_results(): paginated response helper Infrastructure: - OU=Users, OU=Departments, OU=Groups, OU=Positions, OU=Dismissed - Test groups: Developers, Managers - Test departments: IT, HR
This commit is contained in:
parent
3977c99c0c
commit
ad8208789f
246
backend/docs/testing/LDAP_TESTING.md
Normal file
246
backend/docs/testing/LDAP_TESTING.md
Normal file
@ -0,0 +1,246 @@
|
||||
# Инструкция по запуску тестов с LDAP
|
||||
|
||||
## Подготовка тестового окружения
|
||||
|
||||
### 1. Запуск LDAP контейнера
|
||||
|
||||
```bash
|
||||
# Запустить LDAP с профилем
|
||||
docker-compose --profile ldap up -d ldap
|
||||
|
||||
# Проверить что контейнер запущен
|
||||
docker ps | grep eusrr-ldap
|
||||
|
||||
# Просмотр логов
|
||||
docker-compose logs -f ldap
|
||||
```
|
||||
|
||||
### 2. Настройка переменных окружения для тестов
|
||||
|
||||
Скопируйте файл `.env.test` в `.env` или установите переменные:
|
||||
|
||||
```bash
|
||||
cp .env.test .env
|
||||
```
|
||||
|
||||
Ключевые параметры для тестов:
|
||||
```env
|
||||
LDAP_ENABLED=true
|
||||
LDAP_URI=ldap://localhost:389
|
||||
LDAP_BIND_DN=cn=admin,dc=eusrr,dc=local
|
||||
LDAP_BIND_PASSWORD=change-me-redacted-secret
|
||||
LDAP_TLS_REQUIRED=false
|
||||
|
||||
# Базовые DN
|
||||
LDAP_BASE_DN=dc=eusrr,dc=local
|
||||
LDAP_USERS_BASE=OU=Users,dc=eusrr,dc=local
|
||||
LDAP_DEPARTMENTS_BASE=OU=Departments,dc=eusrr,dc=local
|
||||
LDAP_GROUPS_BASE=OU=Groups,dc=eusrr,dc=local
|
||||
LDAP_POSITIONS_BASE=OU=Positions,dc=eusrr,dc=local
|
||||
LDAP_DISMISSED_BASE=OU=Dismissed,dc=eusrr,dc=local
|
||||
```
|
||||
|
||||
### 3. Инициализация LDAP структуры
|
||||
|
||||
LDAP структура инициализируется автоматически при первом запуске контейнера из файла:
|
||||
`backend/scripts/ldap/init/01-base-structure.ldif`
|
||||
|
||||
Структура включает:
|
||||
- OU=Users - пользователи
|
||||
- OU=Departments - отделы
|
||||
- OU=Groups - группы безопасности
|
||||
- OU=Positions - должности
|
||||
- OU=Dismissed - уволенные
|
||||
|
||||
### 4. Проверка подключения к LDAP
|
||||
|
||||
```bash
|
||||
# Из контейнера
|
||||
docker exec -it eusrr-ldap ldapsearch -x -H ldap://localhost -b "dc=eusrr,dc=local" -D "cn=admin,dc=eusrr,dc=local" -w change-me-redacted-secret
|
||||
|
||||
# Или через phpLDAPadmin
|
||||
docker-compose --profile ldap up -d ldap-admin
|
||||
# Открыть http://localhost:8080
|
||||
# Login DN: cn=admin,dc=eusrr,dc=local
|
||||
# Password: change-me-redacted-secret
|
||||
```
|
||||
|
||||
## Запуск тестов
|
||||
|
||||
### Все тесты employees API
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
../.venv/Scripts/python -m pytest tests/api/v1/employees/ -v
|
||||
```
|
||||
|
||||
### Только LDAP тесты
|
||||
|
||||
```bash
|
||||
# Тесты требующие LDAP
|
||||
../.venv/Scripts/python -m pytest tests/api/v1/employees/ -v -m ldap_required
|
||||
|
||||
# Тесты опциональные (работают с LDAP и без)
|
||||
../.venv/Scripts/python -m pytest tests/api/v1/employees/ -v -m ldap_optional
|
||||
```
|
||||
|
||||
### Тесты без LDAP
|
||||
|
||||
```bash
|
||||
# Отключить LDAP для тестов
|
||||
LDAP_ENABLED=false ../.venv/Scripts/python -m pytest tests/api/v1/employees/ -v
|
||||
```
|
||||
|
||||
### Параметризованные тесты (с LDAP и без)
|
||||
|
||||
Многие тесты теперь параметризованы для проверки обоих режимов:
|
||||
|
||||
```python
|
||||
@pytest.mark.ldap_optional
|
||||
@pytest.mark.parametrize("ldap_enabled", [True, False])
|
||||
def test_create_employee(ldap_enabled, settings):
|
||||
settings.LDAP_ENABLED = ldap_enabled
|
||||
# тест работает в обоих режимах
|
||||
```
|
||||
|
||||
## Отладка
|
||||
|
||||
### Просмотр LDAP логов в реальном времени
|
||||
|
||||
```bash
|
||||
docker-compose logs -f ldap
|
||||
```
|
||||
|
||||
### Очистка LDAP данных
|
||||
|
||||
```bash
|
||||
# Остановить и удалить контейнер с данными
|
||||
docker-compose --profile ldap down -v
|
||||
|
||||
# Запустить заново
|
||||
docker-compose --profile ldap up -d ldap
|
||||
```
|
||||
|
||||
### Проверка LDAP операций из Django shell
|
||||
|
||||
```bash
|
||||
../.venv/Scripts/python manage.py shell
|
||||
```
|
||||
|
||||
```python
|
||||
from employees.ldap.infrastructure.connections import _ldap
|
||||
from employees.ldap.directory_service import DirectoryService
|
||||
|
||||
# Проверка подключения
|
||||
with _ldap() as conn:
|
||||
print(conn.bind()) # должно быть True
|
||||
|
||||
# Проверка создания пользователя
|
||||
ds = DirectoryService()
|
||||
# ... тесты операций
|
||||
```
|
||||
|
||||
## Структура тестов
|
||||
|
||||
### Новые фикстуры (tests/ldap_fixtures.py):
|
||||
|
||||
- `ldap_available` - проверяет доступность LDAP
|
||||
- `ldap_config` - конфигурация LDAP для тестов
|
||||
- `ensure_ldap_enabled` - принудительно включает LDAP для теста
|
||||
- `ensure_ldap_disabled` - принудительно выключает LDAP для теста
|
||||
- `ldap_cleanup` - автоматическая очистка после теста
|
||||
|
||||
### Централизованные helpers (tests/api/v1/employees/test_helpers.py):
|
||||
|
||||
- `make_user()` - создание пользователя
|
||||
- `grant_permission()` - выдача разрешений
|
||||
- `make_department()` - создание отдела
|
||||
- `make_position()` - создание должности
|
||||
- `extract_results()` - извлечение из пагинации
|
||||
|
||||
## Маркеры pytest
|
||||
|
||||
- `@pytest.mark.ldap_required` - тест требует LDAP (автоматически скипается если недоступен)
|
||||
- `@pytest.mark.ldap_optional` - тест может работать с LDAP и без
|
||||
|
||||
## Примеры тестов
|
||||
|
||||
### Тест только с LDAP:
|
||||
|
||||
```python
|
||||
@pytest.mark.ldap_required
|
||||
def test_ldap_sync(api_client, ldap_cleanup):
|
||||
# Тест пропускается если LDAP недоступен
|
||||
user = make_user(email="test@example.com")
|
||||
ldap_cleanup.add_for_deletion(user.ldap_dn)
|
||||
# ...
|
||||
```
|
||||
|
||||
### Параметризованный тест:
|
||||
|
||||
```python
|
||||
@pytest.mark.ldap_optional
|
||||
@pytest.mark.parametrize("with_ldap", [True, False])
|
||||
def test_employee_creation(api_client, with_ldap, settings):
|
||||
if with_ldap:
|
||||
settings.LDAP_ENABLED = True
|
||||
else:
|
||||
settings.LDAP_ENABLED = False
|
||||
|
||||
# Тест проверяет оба сценария
|
||||
# ...
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Ошибка: "LDAP server is unavailable"
|
||||
|
||||
1. Проверьте что контейнер запущен: `docker ps | grep ldap`
|
||||
2. Проверьте логи: `docker-compose logs ldap`
|
||||
3. Проверьте порты: `netstat -an | grep 389`
|
||||
4. Перезапустите контейнер: `docker-compose restart ldap`
|
||||
|
||||
### Ошибка: "LDAP_POSITIONS_BASE is not configured"
|
||||
|
||||
Добавьте в `.env`:
|
||||
```env
|
||||
LDAP_POSITIONS_BASE=OU=Positions,dc=eusrr,dc=local
|
||||
```
|
||||
|
||||
### Тесты падают с "AttributeError: 'module' object has no attribute '_is_ldap_enabled'"
|
||||
|
||||
После рефакторинга views разделены на модули. Обновите патчи:
|
||||
```python
|
||||
# Старый путь
|
||||
@patch('api.v1.employees.views._is_ldap_enabled')
|
||||
|
||||
# Новый путь
|
||||
@patch('api.v1.employees.views._helpers._is_ldap_enabled')
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions пример:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
ldap:
|
||||
image: osixia/openldap:1.5.0
|
||||
env:
|
||||
LDAP_DOMAIN: eusrr.local
|
||||
LDAP_ADMIN_PASSWORD: change-me-redacted-secret
|
||||
ports:
|
||||
- 389:389
|
||||
|
||||
steps:
|
||||
- name: Run tests with LDAP
|
||||
env:
|
||||
LDAP_ENABLED: true
|
||||
LDAP_URI: ldap://localhost:389
|
||||
run: pytest tests/api/v1/employees/ -m ldap_required
|
||||
|
||||
- name: Run tests without LDAP
|
||||
env:
|
||||
LDAP_ENABLED: false
|
||||
run: pytest tests/api/v1/employees/ -m "not ldap_required"
|
||||
```
|
||||
@ -12,6 +12,8 @@ markers =
|
||||
integration: marks tests as integration tests
|
||||
slow: marks tests as slow running
|
||||
asyncio: marks tests as asynchronous tests
|
||||
ldap_required: тест требует работающий LDAP сервер
|
||||
ldap_optional: тест может работать с LDAP и без него (параметризован)
|
||||
|
||||
# Исключить manual tests по умолчанию
|
||||
addopts = -m "not manual" --strict-markers
|
||||
|
||||
67
backend/scripts/ldap/init/01-base-structure.ldif
Normal file
67
backend/scripts/ldap/init/01-base-structure.ldif
Normal file
@ -0,0 +1,67 @@
|
||||
# Базовая структура для тестового LDAP
|
||||
# Создается при инициализации контейнера OpenLDAP
|
||||
|
||||
# OU для пользователей (Users)
|
||||
dn: OU=Users,dc=eusrr,dc=local
|
||||
objectClass: organizationalUnit
|
||||
objectClass: top
|
||||
ou: Users
|
||||
description: Контейнер для пользователей EUSRR
|
||||
|
||||
# OU для отделов (Departments)
|
||||
dn: OU=Departments,dc=eusrr,dc=local
|
||||
objectClass: organizationalUnit
|
||||
objectClass: top
|
||||
ou: Departments
|
||||
description: Контейнер для отделов
|
||||
|
||||
# OU для групп (Groups)
|
||||
dn: OU=Groups,dc=eusrr,dc=local
|
||||
objectClass: organizationalUnit
|
||||
objectClass: top
|
||||
ou: Groups
|
||||
description: Контейнер для групп безопасности
|
||||
|
||||
# OU для должностей (Positions)
|
||||
dn: OU=Positions,dc=eusrr,dc=local
|
||||
objectClass: organizationalUnit
|
||||
objectClass: top
|
||||
ou: Positions
|
||||
description: Контейнер для должностей
|
||||
|
||||
# OU для уволенных (Dismissed)
|
||||
dn: OU=Dismissed,dc=eusrr,dc=local
|
||||
objectClass: organizationalUnit
|
||||
objectClass: top
|
||||
ou: Dismissed
|
||||
description: Контейнер для уволенных пользователей
|
||||
|
||||
# Тестовый отдел IT
|
||||
dn: OU=IT,OU=Departments,dc=eusrr,dc=local
|
||||
objectClass: organizationalUnit
|
||||
objectClass: top
|
||||
ou: IT
|
||||
description: IT Department
|
||||
|
||||
# Тестовый отдел HR
|
||||
dn: OU=HR,OU=Departments,dc=eusrr,dc=local
|
||||
objectClass: organizationalUnit
|
||||
objectClass: top
|
||||
ou: HR
|
||||
description: HR Department
|
||||
|
||||
# Группа безопасности - Developers
|
||||
dn: CN=Developers,OU=Groups,dc=eusrr,dc=local
|
||||
objectClass: groupOfNames
|
||||
objectClass: top
|
||||
cn: Developers
|
||||
description: Developers group
|
||||
member: cn=admin,dc=eusrr,dc=local
|
||||
|
||||
# Группа безопасности - Managers
|
||||
dn: CN=Managers,OU=Groups,dc=eusrr,dc=local
|
||||
objectClass: groupOfNames
|
||||
objectClass: top
|
||||
cn: Managers
|
||||
description: Managers group
|
||||
member: cn=admin,dc=eusrr,dc=local
|
||||
130
backend/scripts/update_test_helpers.py
Normal file
130
backend/scripts/update_test_helpers.py
Normal file
@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Скрипт для обновления тестов - заменяет локальные helper функции на импорты из test_helpers.
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Файлы для обработки
|
||||
test_dir = Path("tests/api/v1/employees")
|
||||
|
||||
files_to_update = [
|
||||
"test_department_head_rights.py",
|
||||
"test_department_membership_separation.py",
|
||||
"test_department_roles.py",
|
||||
"test_department_roles_extra.py",
|
||||
"test_departments.py",
|
||||
"test_email_verification_security.py",
|
||||
"test_employee_actions.py",
|
||||
"test_employees.py",
|
||||
"test_employees_fields_in_list.py",
|
||||
"test_ldap_optional_groups.py",
|
||||
"test_positions.py",
|
||||
"test_role_assignment.py",
|
||||
"test_skills.py",
|
||||
]
|
||||
|
||||
def add_import_if_missing(content: str) -> str:
|
||||
"""Добавляет импорт test_helpers если его нет."""
|
||||
import_line = "from tests.api.v1.employees.test_helpers import make_user, grant_permission, make_department, extract_results"
|
||||
|
||||
if "test_helpers" in content:
|
||||
return content
|
||||
|
||||
# Находим последний import
|
||||
import_pattern = r'^(import |from )'
|
||||
lines = content.split('\n')
|
||||
last_import_idx = 0
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if re.match(import_pattern, line):
|
||||
last_import_idx = i
|
||||
|
||||
# Вставляем после последнего импорта
|
||||
lines.insert(last_import_idx + 1, import_line)
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def remove_local_functions(content: str) -> str:
|
||||
"""Удаляет локальные определения helper функций."""
|
||||
|
||||
# Паттерны для удаления
|
||||
patterns = [
|
||||
# make_user fixture
|
||||
r'@pytest\.fixture\s+def make_user\([^)]*\)[^:]*:.*?(?=\n(?:def |class |@pytest|$))',
|
||||
# make_user обычная функция
|
||||
r'def make_user\([^)]*\)[^:]*:.*?(?=\n(?:def |class |@pytest|$))',
|
||||
# _make_user
|
||||
r'def _make_user\([^)]*\)[^:]*:.*?(?=\n(?:def |class |@pytest|$))',
|
||||
# _user
|
||||
r'def _user\([^)]*\)[^:]*:.*?(?=\n(?:def |class |@pytest|$))',
|
||||
# _grant, _grant_perm
|
||||
r'def _grant(?:_perm)?\([^)]*\)[^:]*:.*?(?=\n(?:def |class |@pytest|$))',
|
||||
# _unique_phone, _unique_email
|
||||
r'def _unique_(?:phone|email)\([^)]*\)[^:]*:.*?(?=\n(?:def |class |@pytest|$))',
|
||||
# extract_results
|
||||
r'def extract_results\([^)]*\)[^:]*:.*?(?=\n(?:def |class |@pytest|$))',
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
content = re.sub(pattern, '', content, flags=re.DOTALL | re.MULTILINE)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def replace_function_calls(content: str) -> str:
|
||||
"""Заменяет вызовы локальных функций на импортированные."""
|
||||
replacements = {
|
||||
r'\b_make_user\(': 'make_user(',
|
||||
r'\b_user\(': 'make_user(',
|
||||
r'\b_grant\(': 'grant_permission(',
|
||||
r'\b_grant_perm\(': 'grant_permission(',
|
||||
}
|
||||
|
||||
for old, new in replacements.items():
|
||||
content = re.sub(old, new, content)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def update_file(filepath: Path):
|
||||
"""Обновляет один файл."""
|
||||
print(f"Обновляем {filepath.name}...")
|
||||
|
||||
content = filepath.read_text(encoding='utf-8')
|
||||
|
||||
# Добавляем импорт
|
||||
content = add_import_if_missing(content)
|
||||
|
||||
# Удаляем локальные функции
|
||||
content = remove_local_functions(content)
|
||||
|
||||
# Заменяем вызовы
|
||||
content = replace_function_calls(content)
|
||||
|
||||
# Убираем лишние пустые строки
|
||||
content = re.sub(r'\n{3,}', '\n\n', content)
|
||||
|
||||
filepath.write_text(content, encoding='utf-8')
|
||||
print(f" ✓ {filepath.name} обновлен")
|
||||
|
||||
|
||||
def main():
|
||||
"""Главная функция."""
|
||||
for filename in files_to_update:
|
||||
filepath = test_dir / filename
|
||||
if filepath.exists():
|
||||
try:
|
||||
update_file(filepath)
|
||||
except Exception as e:
|
||||
print(f" ✗ Ошибка в {filename}: {e}", file=sys.stderr)
|
||||
else:
|
||||
print(f" ! {filename} не найден")
|
||||
|
||||
print("\n✅ Обновление завершено!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -8,6 +8,7 @@ from rest_framework import status
|
||||
from employees.models import Department, DepartmentRole, EmployeeDepartment, Employee
|
||||
from tests.conftest import _unique_phone
|
||||
from tests.test_config import DEFAULT_PASSWORD
|
||||
from tests.api.v1.employees.test_helpers import make_user, grant_permission, make_department
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@ -19,28 +20,11 @@ def _unique_phone() -> str:
|
||||
base = 79000000000 # 79 + 9 нулей
|
||||
return str(base + User.objects.count())
|
||||
|
||||
@pytest.fixture
|
||||
def make_user(email: str, staff: bool = False, verified: bool = True) -> User:
|
||||
"""Fixture для создания пользователей."""
|
||||
|
||||
u = User.objects.create_user(
|
||||
email=email,
|
||||
password="pwd12345",
|
||||
phone_number=_unique_phone(),
|
||||
send_activation_email=False,
|
||||
email_verified=True,
|
||||
)
|
||||
# 👇 явная активация для тестов управленческих прав
|
||||
if not u.is_active:
|
||||
u.is_active = True
|
||||
u.save(update_fields=["is_active"])
|
||||
return u
|
||||
# Используем make_user из test_helpers
|
||||
|
||||
def _unique_email() -> str:
|
||||
base = "asd@mail.com"
|
||||
return str(Employee.objects.count()) + base
|
||||
|
||||
def _make_user(staff=False, superuser=False) -> Employee:
|
||||
"""
|
||||
Создаёт активного и 'email_verified' пользователя, чтобы не споткнуться о
|
||||
валидацию при назначении руководителя.
|
||||
|
||||
168
backend/tests/api/v1/employees/test_helpers.py
Normal file
168
backend/tests/api/v1/employees/test_helpers.py
Normal file
@ -0,0 +1,168 @@
|
||||
"""
|
||||
Общие helper-функции для тестов API employees.
|
||||
|
||||
Используйте эти функции вместо создания собственных в каждом тестовом файле.
|
||||
"""
|
||||
import itertools
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Permission
|
||||
from employees.models import Employee, Department, DepartmentRole, Position
|
||||
|
||||
from tests.test_config import DEFAULT_PASSWORD, TEST_EMAIL_DOMAIN
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
# Счетчики для уникальных значений
|
||||
_email_counter = itertools.count(1)
|
||||
_phone_counter = itertools.count(1000)
|
||||
|
||||
|
||||
def make_unique_email(prefix: str = "user") -> str:
|
||||
"""Генерирует уникальный email для тестов."""
|
||||
return f"{prefix}{next(_email_counter)}@{TEST_EMAIL_DOMAIN}"
|
||||
|
||||
|
||||
def make_unique_phone() -> str:
|
||||
"""Генерирует уникальный телефонный номер для тестов."""
|
||||
return f"+7999000{next(_phone_counter):04d}"
|
||||
|
||||
|
||||
def make_user(
|
||||
email: str = None,
|
||||
*,
|
||||
staff: bool = False,
|
||||
superuser: bool = False,
|
||||
verified: bool = True,
|
||||
active: bool = True,
|
||||
password: str = DEFAULT_PASSWORD,
|
||||
**extra
|
||||
) -> User:
|
||||
"""
|
||||
Создает пользователя для тестов.
|
||||
|
||||
Args:
|
||||
email: Email пользователя (если None, генерируется автоматически)
|
||||
staff: Флаг is_staff
|
||||
superuser: Флаг is_superuser
|
||||
verified: Флаг email_verified
|
||||
active: Флаг is_active
|
||||
password: Пароль (по умолчанию DEFAULT_PASSWORD)
|
||||
**extra: Дополнительные поля
|
||||
|
||||
Returns:
|
||||
User: Созданный пользователь
|
||||
"""
|
||||
if email is None:
|
||||
email = make_unique_email()
|
||||
|
||||
if 'phone_number' not in extra:
|
||||
extra['phone_number'] = make_unique_phone()
|
||||
|
||||
if 'first_name' not in extra:
|
||||
extra['first_name'] = 'Test'
|
||||
|
||||
if 'last_name' not in extra:
|
||||
extra['last_name'] = 'User'
|
||||
|
||||
user = User.objects.create(
|
||||
email=email,
|
||||
is_staff=staff,
|
||||
is_superuser=superuser,
|
||||
email_verified=verified,
|
||||
is_active=active,
|
||||
**extra
|
||||
)
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def grant_permission(user: User, permission_codename: str):
|
||||
"""
|
||||
Выдает пользователю указанное разрешение.
|
||||
|
||||
Args:
|
||||
user: Пользователь
|
||||
permission_codename: Codename разрешения в формате 'app_label.codename'
|
||||
или просто 'codename'
|
||||
"""
|
||||
if '.' in permission_codename:
|
||||
app_label, codename = permission_codename.split('.', 1)
|
||||
perm = Permission.objects.get(
|
||||
content_type__app_label=app_label,
|
||||
codename=codename
|
||||
)
|
||||
else:
|
||||
# Пытаемся найти по codename
|
||||
perm = Permission.objects.get(codename=permission_codename)
|
||||
|
||||
user.user_permissions.add(perm)
|
||||
user.save()
|
||||
|
||||
|
||||
def make_department(**kwargs) -> Department:
|
||||
"""
|
||||
Создает отдел для тестов.
|
||||
|
||||
Args:
|
||||
**kwargs: Поля модели Department
|
||||
|
||||
Returns:
|
||||
Department: Созданный отдел
|
||||
"""
|
||||
if 'name' not in kwargs:
|
||||
kwargs['name'] = f"Test Department {Department.objects.count() + 1}"
|
||||
|
||||
return Department.objects.create(**kwargs)
|
||||
|
||||
|
||||
def make_position(**kwargs) -> Position:
|
||||
"""
|
||||
Создает должность для тестов.
|
||||
|
||||
Args:
|
||||
**kwargs: Поля модели Position
|
||||
|
||||
Returns:
|
||||
Position: Созданная должность
|
||||
"""
|
||||
if 'name' not in kwargs:
|
||||
kwargs['name'] = f"Test Position {Position.objects.count() + 1}"
|
||||
|
||||
return Position.objects.create(**kwargs)
|
||||
|
||||
|
||||
def make_department_role(department: Department, **kwargs) -> DepartmentRole:
|
||||
"""
|
||||
Создает роль отдела для тестов.
|
||||
|
||||
Args:
|
||||
department: Отдел
|
||||
**kwargs: Дополнительные поля
|
||||
|
||||
Returns:
|
||||
DepartmentRole: Созданная роль
|
||||
"""
|
||||
if 'name' not in kwargs:
|
||||
kwargs['name'] = f"Test Role {DepartmentRole.objects.count() + 1}"
|
||||
|
||||
return DepartmentRole.objects.create(
|
||||
department=department,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
def extract_results(data):
|
||||
"""
|
||||
Извлекает результаты из пагинированного ответа API.
|
||||
|
||||
Args:
|
||||
data: Данные ответа (dict или list)
|
||||
|
||||
Returns:
|
||||
list: Список результатов
|
||||
"""
|
||||
if isinstance(data, dict) and 'results' in data:
|
||||
return data['results']
|
||||
return data
|
||||
@ -28,6 +28,9 @@ from tests.test_config import (
|
||||
TEST_IMAGE_DATA_URI,
|
||||
)
|
||||
|
||||
# Импортируем LDAP фикстуры
|
||||
from tests.ldap_fixtures import * # noqa
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
137
backend/tests/ldap_fixtures.py
Normal file
137
backend/tests/ldap_fixtures.py
Normal file
@ -0,0 +1,137 @@
|
||||
"""
|
||||
Фикстуры для тестирования с LDAP.
|
||||
|
||||
Предоставляет:
|
||||
- ldap_connection: подключение к тестовому LDAP серверу
|
||||
- ldap_enabled/ldap_disabled: маркеры для параметризации тестов
|
||||
- ldap_config: конфигурация LDAP для тестов
|
||||
"""
|
||||
import os
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def ldap_available():
|
||||
"""
|
||||
Проверяет доступность LDAP сервера.
|
||||
|
||||
Returns:
|
||||
bool: True если LDAP доступен и настроен
|
||||
"""
|
||||
if not settings.LDAP_ENABLED:
|
||||
return False
|
||||
|
||||
try:
|
||||
from employees.ldap.infrastructure.connections import _ldap
|
||||
with _ldap() as conn:
|
||||
# Простая проверка подключения
|
||||
return conn.bind()
|
||||
except Exception as e:
|
||||
print(f"LDAP не доступен: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ldap_config():
|
||||
"""
|
||||
Возвращает конфигурацию LDAP для тестов.
|
||||
"""
|
||||
return {
|
||||
'enabled': settings.LDAP_ENABLED,
|
||||
'uri': settings.LDAP_URI,
|
||||
'base_dn': settings.LDAP_BASE_DN,
|
||||
'users_base': settings.LDAP_USERS_BASE,
|
||||
'departments_base': getattr(settings, 'LDAP_DEPARTMENTS_BASE', None),
|
||||
'groups_base': getattr(settings, 'LDAP_GROUPS_BASE', None),
|
||||
'positions_base': getattr(settings, 'LDAP_POSITIONS_BASE', None),
|
||||
'dismissed_base': getattr(settings, 'LDAP_DISMISSED_BASE', None),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ensure_ldap_enabled(settings):
|
||||
"""
|
||||
Гарантирует что LDAP включен для теста.
|
||||
Используется для тестов которые требуют LDAP.
|
||||
"""
|
||||
original = settings.LDAP_ENABLED
|
||||
settings.LDAP_ENABLED = True
|
||||
yield
|
||||
settings.LDAP_ENABLED = original
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ensure_ldap_disabled(settings):
|
||||
"""
|
||||
Гарантирует что LDAP выключен для теста.
|
||||
Используется для тестов режима без LDAP.
|
||||
"""
|
||||
original = settings.LDAP_ENABLED
|
||||
settings.LDAP_ENABLED = False
|
||||
yield
|
||||
settings.LDAP_ENABLED = original
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ldap_cleanup():
|
||||
"""
|
||||
Cleanup фикстура для удаления тестовых данных из LDAP после теста.
|
||||
|
||||
Usage:
|
||||
def test_something(ldap_cleanup):
|
||||
# создаем объекты в LDAP
|
||||
ldap_cleanup.add_for_deletion(dn)
|
||||
"""
|
||||
cleanup_list = []
|
||||
|
||||
class Cleanup:
|
||||
def add_for_deletion(self, dn):
|
||||
cleanup_list.append(dn)
|
||||
|
||||
yield Cleanup()
|
||||
|
||||
# Cleanup после теста
|
||||
if cleanup_list and settings.LDAP_ENABLED:
|
||||
try:
|
||||
from employees.ldap.infrastructure.connections import _ldap
|
||||
with _ldap() as conn:
|
||||
for dn in reversed(cleanup_list): # удаляем в обратном порядке
|
||||
try:
|
||||
conn.delete(dn)
|
||||
except Exception as e:
|
||||
print(f"Не удалось удалить {dn}: {e}")
|
||||
except Exception as e:
|
||||
print(f"LDAP cleanup failed: {e}")
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""
|
||||
Регистрация маркеров для pytest.
|
||||
"""
|
||||
config.addinivalue_line(
|
||||
"markers", "ldap_required: тест требует работающий LDAP сервер"
|
||||
)
|
||||
config.addinivalue_line(
|
||||
"markers", "ldap_optional: тест может работать с LDAP и без него (параметризован)"
|
||||
)
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
"""
|
||||
Автоматически скипает тесты требующие LDAP если он недоступен.
|
||||
"""
|
||||
ldap_available = False
|
||||
if settings.LDAP_ENABLED:
|
||||
try:
|
||||
from employees.ldap.infrastructure.connections import _ldap
|
||||
with _ldap() as conn:
|
||||
ldap_available = conn.bind()
|
||||
except:
|
||||
pass
|
||||
|
||||
skip_ldap = pytest.mark.skip(reason="LDAP сервер недоступен")
|
||||
|
||||
for item in items:
|
||||
if "ldap_required" in item.keywords and not ldap_available:
|
||||
item.add_marker(skip_ldap)
|
||||
Loading…
x
Reference in New Issue
Block a user