# План тестирования: Опциональная интеграция с LDAP
## Цель тестирования
Проверить корректность работы системы в двух режимах:
1. **LDAP_ENABLED=True** - полная интеграция с LDAP (существующая функциональность)
2. **LDAP_ENABLED=False** - автономная работа без LDAP (новая функциональность)
## Конфигурация окружения
### Режим 1: С LDAP (LDAP_ENABLED=True)
```bash
export LDAP_ENABLED=true
export LDAP_SERVER_URI=ldap://your-ldap-server:389
export LDAP_BIND_DN=CN=admin,DC=example,DC=com
export LDAP_BIND_PASSWORD=your-password
export LDAP_BASE_DN=DC=example,DC=com
export LDAP_USERS_BASE=OU=Users,DC=example,DC=com
export LDAP_GROUPS_BASE=OU=Groups,DC=example,DC=com
export LDAP_UPN_SUFFIX=@example.com
```
### Режим 2: Без LDAP (LDAP_ENABLED=False)
```bash
export LDAP_ENABLED=false
# Все остальные LDAP настройки можно не задавать
```
---
## 1. Тестирование вспомогательных функций
### 1.1 Функция `_is_ldap_enabled()`
| ID | Сценарий | LDAP_ENABLED | Ожидаемый результат |
|----|----------|--------------|---------------------|
| H1 | Проверка с LDAP | True | Возвращает True |
| H2 | Проверка без LDAP | False | Возвращает False |
| H3 | LDAP_ENABLED не задан | (не установлен) | Возвращает False (по умолчанию) |
### 1.2 Функция `_ldap_try(fn)`
| ID | Сценарий | LDAP_ENABLED | Действие | Ожидаемый результат |
|----|----------|--------------|----------|---------------------|
| H4 | Успешное выполнение | True | fn() успешна | Возвращает None |
| H5 | Ошибка LDAP | True | fn() бросает DirectoryLdapError | Возвращает Response(502) |
| H6 | LDAP отключен | False | fn() не вызывается | Возвращает None |
| H7 | Ошибка DirectoryServiceError | True | fn() бросает DirectoryServiceError | Возвращает Response(502) |
---
## 2. Тестирование RegisterAPIView
### 2.1 Регистрация пользователя (POST /api/v1/register/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Тест-данные | Ожидаемый результат |
|----|----------|-------------|---------------------|
| R1 | Успешная регистрация | Валидные данные (email, пароль, имя, телефон) | • Создан пользователь в LDAP (disabled)
• Пароль установлен в LDAP
• Создана запись в БД (unusable_password)
• is_active=False
• Отправлено письмо верификации
• HTTP 201 |
| R2 | Регистрация с аватаром | + файл avatar | • Аватар загружен в LDAP
• Сохранён в БД
• HTTP 201 |
| R3 | Дублирование email | Email уже существует | HTTP 400 "email_exists" |
| R4 | Дублирование телефона | Телефон уже существует | HTTP 400 "phone_exists" |
| R5 | LDAP недоступен | LDAP сервер не отвечает | HTTP 502 "ldap_error" |
| R6 | Ошибка создания в БД | БД недоступна после LDAP | • LDAP пользователь удалён (rollback)
• HTTP 500 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Тест-данные | Ожидаемый результат |
|----|----------|-------------|---------------------|
| R7 | Успешная регистрация | Валидные данные | • Создан пользователь только в БД
• Пароль установлен в БД (set_password)
• is_active=False
• Письмо верификации отправлено
• HTTP 201 |
| R8 | Регистрация с аватаром | + файл avatar | • Аватар сохранён в media/
• HTTP 201 |
| R9 | Дублирование данных | Email/телефон существует | HTTP 400 |
**Проверки после регистрации:**
```python
# С LDAP
assert Employee.objects.filter(email=test_email).exists()
assert not user.has_usable_password() # Пароль в LDAP
assert LdapSyncState.objects.filter(model='employee', object_pk=user.id).exists()
# Без LDAP
assert Employee.objects.filter(email=test_email).exists()
assert user.has_usable_password() # Пароль в БД
assert user.check_password(test_password)
```
---
## 3. Тестирование VerifyEmailAPIView
### 3.1 Верификация email (POST /api/v1/verify-email/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Предусловия | Ожидаемый результат |
|----|----------|-------------|---------------------|
| V1 | Успешная верификация | Пользователь существует, не верифицирован | • is_active=True в БД
• Активирован в LDAP (enabled)
• HTTP 200 |
| V2 | Повторная верификация | Уже верифицирован | HTTP 400 "already_verified" |
| V3 | Неверный токен | Токен не существует/истёк | HTTP 404 "user_not_found" |
| V4 | LDAP ошибка активации | LDAP недоступен | HTTP 502 с деталями ошибки |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Предусловия | Ожидаемый результат |
|----|----------|-------------|---------------------|
| V5 | Успешная верификация | Пользователь не верифицирован | • is_active=True в БД
• HTTP 200 |
| V6 | Повторная верификация | Уже верифицирован | HTTP 400 |
**Проверки после верификации:**
```python
# С LDAP
assert user.is_active
assert ldap_user_is_enabled(user.ldap_dn)
# Без LDAP
assert user.is_active
assert user.check_password(original_password) # Пароль сохранён
```
---
## 4. Тестирование DepartmentViewSet
### 4.1 Создание отдела (POST /api/v1/departments/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Тест-данные | Ожидаемый результат |
|----|----------|-------------|---------------------|
| D1 | Создание отдела | name, description | • OU создан в LDAP
• Запись в БД
• HTTP 201 |
| D2 | Создание с главой | + head_id | • OU создан
• head установлен через DirectoryService
• HTTP 201 |
| D3 | LDAP ошибка | LDAP недоступен | HTTP 502 |
| D4 | Ошибка БД после LDAP | БД недоступна | • OU удалён (rollback)
• HTTP 500 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Тест-данные | Ожидаемый результат |
|----|----------|-------------|---------------------|
| D5 | Создание отдела | name, description | • Только запись в БД
• HTTP 201 |
| D6 | Создание с главой | + head_id | • Запись в БД с head
• HTTP 201 |
### 4.2 Обновление отдела (PATCH /api/v1/departments/{id}/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Изменения | Ожидаемый результат |
|----|----------|-----------|---------------------|
| D7 | Изменение имени | name="New Name" | • OU переименован в LDAP
• Обновлено в БД
• HTTP 200 |
| D8 | Смена главы | head_id=new_head | • set_head() в LDAP
• head_appointed_at установлен
• HTTP 200 |
| D9 | Удаление главы | head_id=null | • set_head(None) в LDAP
• head=null в БД
• HTTP 200 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Изменения | Ожидаемый результат |
|----|----------|-----------|---------------------|
| D10 | Изменение имени | name="New Name" | • Обновлено только в БД
• HTTP 200 |
| D11 | Смена главы | head_id=new_head | • head обновлён в БД
• head_appointed_at установлен
• HTTP 200 |
### 4.3 Управление участниками отдела
#### add_member (POST /api/v1/departments/{id}/add-member/)
##### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| D12 | Добавление участника | employee_id | • Пользователь MOVE в OU отдела в LDAP
• DepartmentMember создан/активирован
• HTTP 200 |
| D13 | Повторное добавление | Уже участник | • Активация существующей связи
• HTTP 200 |
| D14 | LDAP ошибка | LDAP недоступен | HTTP 502 |
##### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| D15 | Добавление участника | employee_id | • DepartmentMember создан/активирован в БД
• HTTP 200 |
#### remove_member (POST /api/v1/departments/{id}/remove-member/)
##### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| D16 | Удаление участника | employee_id | • Пользователь MOVE обратно в Users OU
• is_active=False в DepartmentMember
• HTTP 200 |
| D17 | Удаление несуществующего | Нет такого участника | HTTP 404 |
##### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| D18 | Удаление участника | employee_id | • is_active=False в БД
• HTTP 200 |
### 4.4 set_member_role (POST /api/v1/departments/{id}/set-member-role/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| D19 | Назначение роли | employee_id, role_id | • Роль назначена в LDAP (группы)
• role обновлена в БД
• HTTP 200 |
| D20 | Снятие роли | employee_id, role_id=null | • Роли удалены в LDAP
• role=null в БД
• HTTP 200 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| D21 | Назначение роли | employee_id, role_id | • role обновлена только в БД
• HTTP 200 |
### 4.5 Удаление отдела (DELETE /api/v1/departments/{id}/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Условия | Ожидаемый результат |
|----|----------|---------|---------------------|
| D22 | Удаление пустого отдела | Нет участников | • OU удалён в LDAP
• Удалено из БД
• HTTP 204 |
| D23 | Удаление с участниками | Есть активные участники | • OU удалён
• Участники перемещены в Users OU
• HTTP 204 |
| D24 | LDAP ошибка без force_db | LDAP недоступен, force_db=false | HTTP 502 |
| D25 | LDAP ошибка с force_db | LDAP недоступен, force_db=true | • БД удалена несмотря на LDAP
• HTTP 204 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Условия | Ожидаемый результат |
|----|----------|---------|---------------------|
| D26 | Удаление отдела | Любые условия | • Удалено из БД
• HTTP 204 |
---
## 5. Тестирование EmployeeViewSet
### 5.1 Создание сотрудника (POST /api/v1/employees/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| E1 | Создание базовое | email, имя, пароль | • Пользователь в LDAP
• Запись в БД
• HTTP 201 |
| E2 | Создание с позицией | + position_id | • Создан в LDAP
• assign_position() вызван
• Группы позиции назначены
• HTTP 201 |
| E3 | Создание с аватаром | + avatar file | • Аватар в LDAP
• HTTP 201 |
| E4 | LDAP ошибка | LDAP недоступен | HTTP 502 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| E5 | Создание базовое | email, имя, пароль | • Только в БД
• Пароль через set_password()
• HTTP 201 |
| E6 | Создание с позицией | + position_id | • Создан в БД
• position установлена
• Группы через M2M
• HTTP 201 |
### 5.2 Обновление сотрудника (PATCH /api/v1/employees/{id}/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Изменения | Ожидаемый результат |
|----|----------|-----------|---------------------|
| E7 | Обновление имени | first_name, last_name | • update_user() в LDAP
• Обновлено в БД
• HTTP 200 |
| E8 | Смена email | email="new@mail.com" | • Email обновлён в LDAP
• Обновлён в БД
• HTTP 200 |
| E9 | Смена позиции | position_id=new_position | • Позиция обновлена в LDAP
• Группы пересчитаны
• HTTP 200 |
| E10 | Удаление позиции | position=null | • Позиция удалена в LDAP
• position=null в БД
• HTTP 200 |
| E11 | Обновление аватара | + новый avatar | • Аватар обновлён в LDAP
• Сохранён в БД
• HTTP 200 |
| E12 | Перемещение в отдел | department_dn=dept_dn | • MOVE в LDAP
• HTTP 200 |
| E13 | Назначение групп | group_cns=['CN=Group1',...] | • Группы назначены в LDAP
• HTTP 200 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Изменения | Ожидаемый результат |
|----|----------|-----------|---------------------|
| E14 | Обновление всех полей | first_name, last_name, email, phone, position, avatar | • Все обновлено в БД
• Аватар сохранён в media/
• HTTP 200 |
| E15 | Смена позиции | position_id=new_position | • Позиция обновлена в БД
• Группы через M2M
• HTTP 200 |
**Проверки:**
```python
# С LDAP
user = Employee.objects.get(id=test_id)
ldap_user = DirectoryService().get_user_by_dn(user.ldap_dn)
assert ldap_user['givenName'] == user.first_name
assert ldap_user['sn'] == user.last_name
# Без LDAP
user = Employee.objects.get(id=test_id)
assert user.first_name == expected_first_name
assert user.position_id == expected_position_id
```
---
## 6. Тестирование PositionViewSet
### 6.1 CRUD позиций
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Действие | Ожидаемый результат |
|----|----------|----------|---------------------|
| P1 | Создание позиции | POST /api/v1/positions/ | • Позиция в БД
• reconcile_position() вызван
• Группа создана в LDAP
• HTTP 201 |
| P2 | Обновление позиции | PATCH /api/v1/positions/{id}/ | • Обновлено в БД
• reconcile_position() вызван
• HTTP 200 |
| P3 | Удаление позиции | DELETE /api/v1/positions/{id}/ | • delete_position_group() вызван
• Удалено из БД
• HTTP 204 |
| P4 | Назначение групп | POST /positions/{id}/set-groups | • groups.set() в БД
• reconcile_position() синхронизирует LDAP
• HTTP 200 |
| P5 | Добавление групп | POST /positions/{id}/add-groups | • groups.add() в БД
• reconcile_position()
• HTTP 200 |
| P6 | Удаление групп | POST /positions/{id}/remove-groups | • groups.remove() в БД
• reconcile_position()
• HTTP 200 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Действие | Ожидаемый результат |
|----|----------|----------|---------------------|
| P7 | Создание позиции | POST /api/v1/positions/ | • Только в БД
• LDAP не вызывается
• HTTP 201 |
| P8 | Обновление позиции | PATCH /api/v1/positions/{id}/ | • Обновлено в БД
• HTTP 200 |
| P9 | Удаление позиции | DELETE /api/v1/positions/{id}/ | • Удалено из БД
• HTTP 204 |
| P10 | Управление группами | set/add/remove-groups | • Только M2M в БД
• HTTP 200 |
**Проверки _ldap_try():**
```python
# _ldap_try() должен возвращать None в обоих случаях:
# - LDAP_ENABLED=True и операция успешна → None
# - LDAP_ENABLED=False → None (операция пропущена)
# Это не должно блокировать дальнейшее выполнение
```
---
## 7. Тестирование GroupViewSet
### 7.1 Создание группы (POST /api/v1/groups/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G1 | Создание группы | name, description | • Группа создана в LDAP
• Запись в БД
• HTTP 201 |
| G2 | С permissions | + permissions=[1,2,3] | • Группа в LDAP
• Permissions в БД
• HTTP 201 |
| G3 | LDAP ошибка | LDAP недоступен | HTTP 502 |
| G4 | Ошибка БД | БД сбой после LDAP | • Группа удалена из LDAP (rollback)
• HTTP 500 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G5 | Создание группы | name, description, permissions | • Только в БД
• Permissions через M2M
• HTTP 201 |
### 7.2 Обновление группы (PATCH /api/v1/groups/{id}/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Изменения | Ожидаемый результат |
|----|----------|-----------|---------------------|
| G6 | Переименование | name="New Name" | • group_rename() в LDAP
• Обновлено в БД
• HTTP 200 |
| G7 | Изменение описания | ldap_description="..." | • group_set_description() в LDAP
• HTTP 200 |
| G8 | Группа не найдена в LDAP | _resolve_group_dn() → None | HTTP 404 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Изменения | Ожидаемый результат |
|----|----------|-----------|---------------------|
| G9 | Переименование | name="New Name" | • Обновлено в БД
• HTTP 200 |
### 7.3 Action методы группы
#### rename (POST /api/v1/groups/{id}/rename/)
##### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G10 | Переименование | new_name="NewName" | • group_rename() в LDAP
• name обновлён в БД
• HTTP 200 |
| G11 | LDAP ошибка | LDAP недоступен | HTTP 502 |
##### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G12 | Переименование | new_name="NewName" | • Обновлено в БД
• HTTP 200 |
#### set_description (POST /api/v1/groups/{id}/set-description/)
##### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G13 | Установка описания | description="..." | • group_set_description() в LDAP
• HTTP 200 |
##### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G14 | Установка описания | description="..." | • Ничего не происходит (нет поля в БД)
• HTTP 200 |
#### members (GET /api/v1/groups/{id}/members/)
##### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Условия | Ожидаемый результат |
|----|----------|---------|---------------------|
| G15 | Получение участников | Группа существует | • group_list_members() из LDAP
• Сопоставление с БД через LdapSyncState
• HTTP 200 {"dns": [...], "employees": [...]} |
##### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Условия | Ожидаемый результат |
|----|----------|---------|---------------------|
| G16 | Получение участников | Группа существует | • Участники из БД (grp.user_set.all())
• HTTP 200 {"dns": [], "employees": [...]} |
#### add_members (POST /api/v1/groups/{id}/add-members/)
##### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G17 | Добавление через DN | member_dns=['CN=User1,...'] | • group_add_members() в LDAP
• user_set.add() в БД
• HTTP 200 |
| G18 | Добавление через ID | member_ids=[1,2,3] | • ID→DN преобразование
• Добавлены в LDAP и БД
• HTTP 200 |
| G19 | Ошибка БД | БД сбой после LDAP | • group_remove_members() (rollback)
• HTTP 500 |
##### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G20 | Добавление участников | member_ids=[1,2,3] | • user_set.add() в БД
• HTTP 200 |
#### remove_members (POST /api/v1/groups/{id}/remove-members/)
##### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G21 | Удаление участников | member_dns/member_ids | • group_remove_members() в LDAP
• user_set.remove() в БД
• HTTP 200 |
| G22 | Ошибка БД | БД сбой после LDAP | • group_add_members() (rollback)
• HTTP 500 |
##### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G23 | Удаление участников | member_ids=[1,2,3] | • user_set.remove() в БД
• HTTP 200 |
#### replace_members (POST /api/v1/groups/{id}/replace-members/)
##### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G24 | Замена состава | member_dns/member_ids | • Сохранён старый состав
• group_replace_members() в LDAP
• user_set.set() в БД
• HTTP 200 |
| G25 | Ошибка БД | БД сбой после LDAP | • group_replace_members(prev_dns) (rollback)
• HTTP 500 |
##### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G26 | Замена состава | member_ids=[...] | • user_set.set() в БД
• HTTP 200 |
### 7.4 Удаление группы (DELETE /api/v1/groups/{id}/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Параметры | Ожидаемый результат |
|----|----------|-----------|---------------------|
| G27 | Удаление группы | force_db=false | • group_delete() в LDAP
• Удалено из БД
• HTTP 204 |
| G28 | LDAP ошибка без force | force_db=false, LDAP недоступен | HTTP 502 |
| G29 | LDAP ошибка с force | force_db=true, LDAP недоступен | • Игнорируем LDAP
• Удалено из БД
• HTTP 204 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Параметры | Ожидаемый результат |
|----|----------|-----------|---------------------|
| G30 | Удаление группы | Любые | • Удалено из БД
• HTTP 204 |
### 7.5 Список групп (GET /api/v1/groups/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Условия | Ожидаемый результат |
|----|----------|---------|---------------------|
| G31 | Получение списка | Любые | • sync_groups_catalog() вызван (throttled)
• Список из БД
• HTTP 200 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Условия | Ожидаемый результат |
|----|----------|---------|---------------------|
| G32 | Получение списка | Любые | • Список из БД
• HTTP 200 |
---
## 8. Интеграционные тесты
### 8.1 Сквозной сценарий: Регистрация → Верификация → Назначение позиции
#### С LDAP
| Шаг | Действие | Проверка |
|-----|----------|----------|
| 1 | POST /api/v1/register/ | Пользователь в LDAP (disabled) и БД (inactive) |
| 2 | POST /api/v1/verify-email/ | Активирован в LDAP и БД |
| 3 | PATCH /api/v1/employees/{id}/ (position_id) | Группы позиции назначены в LDAP |
| 4 | GET /api/v1/employees/{id}/ | position корректна, is_active=true |
#### Без LDAP
| Шаг | Действие | Проверка |
|-----|----------|----------|
| 1 | POST /api/v1/register/ | Пользователь только в БД, пароль установлен |
| 2 | POST /api/v1/verify-email/ | is_active=true в БД |
| 3 | PATCH /api/v1/employees/{id}/ (position_id) | Позиция обновлена, группы через M2M |
| 4 | Аутентификация | EmailOrPhoneBackend работает, пароль проверяется из БД |
### 8.2 Сквозной сценарий: Создание отдела → Добавление участника → Назначение роли
#### С LDAP
| Шаг | Действие | Проверка |
|-----|----------|----------|
| 1 | POST /api/v1/departments/ | OU создан в LDAP |
| 2 | POST /departments/{id}/add-member/ | Пользователь перемещён в OU |
| 3 | POST /departments/{id}/set-member-role/ | Роли назначены через LDAP группы |
#### Без LDAP
| Шаг | Действие | Проверка |
|-----|----------|----------|
| 1 | POST /api/v1/departments/ | Запись в БД |
| 2 | POST /departments/{id}/add-member/ | DepartmentMember создан в БД |
| 3 | POST /departments/{id}/set-member-role/ | role обновлена в БД |
### 8.3 Сценарий: Создание группы → Добавление участников → Проверка permissions
#### С LDAP
| Шаг | Действие | Проверка |
|-----|----------|----------|
| 1 | POST /api/v1/groups/ (+ permissions) | Группа в LDAP, permissions в БД |
| 2 | POST /groups/{id}/add-members/ | Участники добавлены в LDAP группу и БД |
| 3 | Проверка прав пользователя | user.has_perm() работает корректно |
#### Без LDAP
| Шаг | Действие | Проверка |
|-----|----------|----------|
| 1 | POST /api/v1/groups/ (+ permissions) | Группа и permissions в БД |
| 2 | POST /groups/{id}/add-members/ | user_set.add() в БД |
| 3 | Проверка прав | user.has_perm() работает через Django permissions |
---
## 9. Тесты граничных условий и ошибок
### 9.1 Переключение режима во время работы
| ID | Сценарий | Действия | Ожидаемый результат |
|----|----------|----------|---------------------|
| B1 | LDAP→No LDAP | 1. Создать пользователя с LDAP
2. Отключить LDAP
3. Обновить пользователя | • Обновление работает только с БД
• Данные не теряются |
| B2 | No LDAP→LDAP | 1. Создать пользователя без LDAP
2. Включить LDAP
3. Обновить пользователя | • Обновление пытается синхронизировать с LDAP
• Может вернуть 502 если пользователя нет в LDAP |
### 9.2 Частичные сбои LDAP
| ID | Сценарий | Условия | Ожидаемый результат |
|----|----------|---------|---------------------|
| B3 | LDAP timeout | LDAP_ENABLED=True, LDAP не отвечает | HTTP 502 с деталями ошибки |
| B4 | LDAP частичный сбой | Создание в LDAP OK, обновление FAIL | Зависит от операции - либо rollback, либо 502 |
| B5 | БД сбой после LDAP | LDAP OK, БД FAIL | Rollback LDAP операции, HTTP 500 |
### 9.3 Некорректные данные
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| B6 | LDAP_ENABLED не boolean | "yes"/"no"/"1"/"0" | Корректно обрабатывается через .lower() == "true" |
| B7 | Отсутствующие LDAP настройки | LDAP_ENABLED=True, но нет SERVER_URI | Ошибка при инициализации DirectoryService |
| B8 | Некорректный DN | Попытка работы с несуществующим DN | HTTP 404 или 502 в зависимости от метода |
---
## 10. Тесты производительности
### 10.1 Массовые операции
| ID | Сценарий | Параметры | Проверка |
|----|----------|-----------|----------|
| P1 | Массовая регистрация | 100 пользователей | • С LDAP: время создания < 30s
• Без LDAP: время < 10s |
| P2 | Добавление 50 участников в группу | 50 member_ids | • Операция выполняется атомарно
• При ошибке - полный rollback |
| P3 | Создание 20 отделов | 20 departments | • С LDAP: все OU созданы
• Без LDAP: только БД |
### 10.2 Нагрузочное тестирование
| ID | Сценарий | Условия | Цель |
|----|----------|---------|------|
| P4 | Concurrent создание пользователей | 10 параллельных запросов | Нет race conditions |
| P5 | Обновление одного пользователя | 5 параллельных PATCH | Последовательность транзакций корректна |
---
## 11. Автоматизация тестирования
### 11.1 Структура тестов
```
tests/
├── test_ldap_optional/
│ ├── __init__.py
│ ├── conftest.py # Фикстуры для двух режимов
│ ├── test_helpers.py # Тесты вспомогательных функций
│ ├── test_register.py # RegisterAPIView
│ ├── test_verify_email.py # VerifyEmailAPIView
│ ├── test_departments.py # DepartmentViewSet
│ ├── test_employees.py # EmployeeViewSet
│ ├── test_positions.py # PositionViewSet
│ ├── test_groups.py # GroupViewSet
│ ├── test_integration.py # Сквозные сценарии
│ └── test_edge_cases.py # Граничные условия
```
### 11.2 Фикстуры pytest
```python
# conftest.py
import pytest
from django.conf import settings
@pytest.fixture(params=[True, False], ids=["with_ldap", "without_ldap"])
def ldap_mode(request, settings):
"""Параметризованная фикстура для тестирования обоих режимов"""
original_value = getattr(settings, 'LDAP_ENABLED', False)
settings.LDAP_ENABLED = request.param
yield request.param
settings.LDAP_ENABLED = original_value
@pytest.fixture
def with_ldap(settings):
"""Фикстура для тестов только с LDAP"""
settings.LDAP_ENABLED = True
yield
settings.LDAP_ENABLED = False
@pytest.fixture
def without_ldap(settings):
"""Фикстура для тестов только без LDAP"""
settings.LDAP_ENABLED = False
yield
@pytest.fixture
def test_user_data():
"""Базовые данные для создания тестового пользователя"""
return {
"email": "test@example.com",
"first_name": "Test",
"last_name": "User",
"phone_number": "+79001234567",
"password": "SecurePass123!",
"whatsapp": "+79001234567"
}
```
### 11.3 Примеры тестов
```python
# test_register.py
import pytest
from rest_framework.test import APIClient
from employees.models import Employee
@pytest.mark.django_db
class TestRegisterAPIView:
def test_register_user_both_modes(self, ldap_mode, test_user_data):
"""R1, R7: Регистрация в обоих режимах"""
client = APIClient()
response = client.post('/api/v1/register/', test_user_data)
assert response.status_code == 201
user = Employee.objects.get(email=test_user_data['email'])
assert user.is_active is False
if ldap_mode: # LDAP_ENABLED=True
assert not user.has_usable_password() # Пароль в LDAP
# Проверка создания в LDAP
from employees.ldap.services import DirectoryService
svc = DirectoryService()
ldap_user = svc.get_user_by_email(test_user_data['email'])
assert ldap_user is not None
else: # LDAP_ENABLED=False
assert user.has_usable_password() # Пароль в БД
assert user.check_password(test_user_data['password'])
def test_register_with_avatar_with_ldap(self, with_ldap, test_user_data):
"""R2: Регистрация с аватаром (LDAP)"""
client = APIClient()
with open('test_files/avatar.jpg', 'rb') as avatar:
test_user_data['avatar'] = avatar
response = client.post('/api/v1/register/', test_user_data, format='multipart')
assert response.status_code == 201
user = Employee.objects.get(email=test_user_data['email'])
assert user.avatar # Аватар сохранён
def test_register_duplicate_email(self, ldap_mode, test_user_data):
"""R3, R9: Дублирование email"""
client = APIClient()
Employee.objects.create_user(**test_user_data)
response = client.post('/api/v1/register/', test_user_data)
assert response.status_code == 400
```
```python
# test_groups.py
import pytest
from rest_framework.test import APIClient
from django.contrib.auth.models import Group
@pytest.mark.django_db
class TestGroupViewSet:
def test_create_group_both_modes(self, ldap_mode, admin_user):
"""G1, G5: Создание группы"""
client = APIClient()
client.force_authenticate(user=admin_user)
data = {"name": "TestGroup", "ldap_description": "Test description"}
response = client.post('/api/v1/groups/', data)
assert response.status_code == 201
group = Group.objects.get(name="TestGroup")
assert group is not None
if ldap_mode:
# Проверка создания в LDAP
from employees.ldap.services import DirectoryService
svc = DirectoryService()
dn = svc.group_find_dn("TestGroup")
assert dn is not None
def test_add_members_both_modes(self, ldap_mode, admin_user, test_group, test_users):
"""G17, G20: Добавление участников"""
client = APIClient()
client.force_authenticate(user=admin_user)
member_ids = [u.id for u in test_users[:3]]
response = client.post(
f'/api/v1/groups/{test_group.id}/add-members/',
{"member_ids": member_ids}
)
assert response.status_code == 200
assert test_group.user_set.count() == 3
if ldap_mode:
# Проверка в LDAP
from employees.ldap.services import DirectoryService
svc = DirectoryService()
dn = svc.group_find_dn(test_group.name)
members = svc.group_list_members(dn)
assert len(members) == 3
```
### 11.4 Команды для запуска тестов
```bash
# Все тесты в обоих режимах
pytest tests/test_ldap_optional/ -v
# Только тесты с LDAP
pytest tests/test_ldap_optional/ -v -k "with_ldap"
# Только тесты без LDAP
pytest tests/test_ldap_optional/ -v -k "without_ldap"
# Конкретный тестовый файл
pytest tests/test_ldap_optional/test_register.py -v
# С покрытием кода
pytest tests/test_ldap_optional/ --cov=api.v1.employees.views --cov-report=html
# Параллельный запуск (быстрее)
pytest tests/test_ldap_optional/ -v -n auto
```
---
## 12. Ручное тестирование
### 12.1 Чек-лист для ручной проверки
#### Подготовка
- [ ] Создать .env файл с LDAP_ENABLED=true
- [ ] Создать .env файл с LDAP_ENABLED=false
- [ ] Поднять LDAP сервер (для режима с LDAP)
- [ ] Запустить миграции БД
#### Регистрация и верификация
- [ ] Зарегистрировать пользователя с LDAP
- [ ] Зарегистрировать пользователя без LDAP
- [ ] Верифицировать email в обоих режимах
- [ ] Попытаться войти после верификации
#### Отделы
- [ ] Создать отдел с LDAP
- [ ] Создать отдел без LDAP
- [ ] Добавить участника в отдел (оба режима)
- [ ] Удалить участника из отдела (оба режима)
- [ ] Назначить роль участнику (оба режима)
- [ ] Удалить отдел (оба режима)
#### Сотрудники
- [ ] Создать сотрудника через админ-панель (оба режима)
- [ ] Обновить имя/email/телефон (оба режима)
- [ ] Назначить позицию (оба режима)
- [ ] Загрузить аватар (оба режима)
#### Позиции
- [ ] Создать позицию (оба режима)
- [ ] Назначить группы позиции (оба режима)
- [ ] Удалить позицию (оба режима)
#### Группы
- [ ] Создать группу (оба режима)
- [ ] Переименовать группу (оба режима)
- [ ] Добавить участников в группу (оба режима)
- [ ] Удалить участников из группы (оба режима)
- [ ] Удалить группу (оба режима)
### 12.2 Инструменты для ручного тестирования
**Postman Collection:**
- Импортировать коллекцию API endpoints
- Создать окружения для LDAP/No-LDAP
- Выполнить последовательность запросов
**LDAP Browser (для режима с LDAP):**
- Apache Directory Studio
- JXplorer
- Проверка структуры OU, пользователей, групп
**Django Admin:**
- Проверка записей в БД
- Сравнение с LDAP (в режиме с LDAP)
---
## 13. Критерии успешности тестирования
### 13.1 Обязательные критерии (PASS/FAIL)
✅ **PASS критерии:**
1. Все автоматические тесты проходят в обоих режимах (≥95% успешности)
2. Нет критических ошибок (500) при корректных запросах
3. Данные в БД консистентны после всех операций
4. В режиме с LDAP: данные синхронизированы между LDAP и БД
5. В режиме без LDAP: все операции работают без вызовов DirectoryService
6. Rollback механизмы работают корректно при ошибках
7. Аутентификация работает в обоих режимах
### 13.2 Метрики качества
| Метрика | Целевое значение |
|---------|------------------|
| Покрытие кода тестами | ≥ 90% |
| Успешность автотестов | 100% |
| Время выполнения всех тестов | < 5 минут |
| Количество критических багов | 0 |
| Количество major багов | ≤ 2 |
| Производительность (регистрация с LDAP) | < 3s на пользователя |
| Производительность (регистрация без LDAP) | < 1s на пользователя |
### 13.3 Тест-репорт
После завершения тестирования создать отчёт:
```markdown
# Отчёт о тестировании LDAP Optional Integration
## Окружение
- Django версия: X.X.X
- Python версия: X.X.X
- LDAP сервер: OpenLDAP X.X.X / AD
## Результаты автотестов
- Всего тестов: XXX
- Успешно: XXX (XX%)
- Провалено: XX (XX%)
- Пропущено: XX
## Покрытие кода
- Общее: XX%
- views.py: XX%
## Найденные баги
1. [CRITICAL] Описание бага...
2. [MAJOR] Описание бага...
## Рекомендации
- Улучшение 1...
- Улучшение 2...
## Заключение
✅ Система готова к развёртыванию / ❌ Требуется доработка
```
---
## 14. Continuous Integration (CI)
### 14.1 GitHub Actions / GitLab CI
```yaml
# .github/workflows/test_ldap_optional.yml
name: Test LDAP Optional Integration
on: [push, pull_request]
jobs:
test-with-ldap:
runs-on: ubuntu-latest
services:
ldap:
image: osixia/openldap:latest
ports:
- 389:389
env:
LDAP_ORGANISATION: "Test Org"
LDAP_DOMAIN: "example.com"
LDAP_ADMIN_PASSWORD: "admin"
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Run tests with LDAP
env:
LDAP_ENABLED: true
LDAP_SERVER_URI: ldap://localhost:389
run: |
pytest tests/test_ldap_optional/ -v -k "with_ldap"
test-without-ldap:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Run tests without LDAP
env:
LDAP_ENABLED: false
run: |
pytest tests/test_ldap_optional/ -v -k "without_ldap"
```
---
## 15. Заключение
Этот план тестирования обеспечивает:
1. **Полноту покрытия** - все 50+ методов во всех ViewSets
2. **Двойное тестирование** - каждая функция в обоих режимах
3. **Автоматизацию** - pytest тесты для CI/CD
4. **Документированность** - четкие test case ID и ожидаемые результаты
5. **Воспроизводимость** - фикстуры и примеры кода
6. **Масштабируемость** - легко добавить новые тест-кейсы
**Следующие шаги:**
1. Реализовать базовые автотесты (conftest.py, test_helpers.py)
2. Постепенно покрыть каждый ViewSet тестами
3. Настроить CI для автоматического прогона
4. Провести ручное тестирование по чек-листу
5. Исправить найденные баги
6. Создать итоговый отчёт