Remove outdated test files and fixtures

This commit is contained in:
igor_nadein 2026-01-21 13:13:44 +03:00
parent ae340f4d03
commit 624784b788
67 changed files with 0 additions and 10414 deletions

View File

@ -1,916 +0,0 @@
# План тестирования: Опциональная интеграция с 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)<br>• Пароль установлен в LDAP<br>• Создана запись в БД (unusable_password)<br>• is_active=False<br>• Отправлено письмо верификации<br>• HTTP 201 |
| R2 | Регистрация с аватаром | + файл avatar | • Аватар загружен в LDAP<br>• Сохранён в БД<br>• 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)<br>• HTTP 500 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Тест-данные | Ожидаемый результат |
|----|----------|-------------|---------------------|
| R7 | Успешная регистрация | Валидные данные | • Создан пользователь только в БД<br>• Пароль установлен в БД (set_password)<br>• is_active=False<br>• Письмо верификации отправлено<br>• HTTP 201 |
| R8 | Регистрация с аватаром | + файл avatar | • Аватар сохранён в media/<br>• 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 в БД<br>• Активирован в LDAP (enabled)<br>• 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 в БД<br>• 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<br>• Запись в БД<br>• HTTP 201 |
| D2 | Создание с главой | + head_id | • OU создан<br>• head установлен через DirectoryService<br>• HTTP 201 |
| D3 | LDAP ошибка | LDAP недоступен | HTTP 502 |
| D4 | Ошибка БД после LDAP | БД недоступна | • OU удалён (rollback)<br>• HTTP 500 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Тест-данные | Ожидаемый результат |
|----|----------|-------------|---------------------|
| D5 | Создание отдела | name, description | • Только запись в БД<br>• HTTP 201 |
| D6 | Создание с главой | + head_id | • Запись в БД с head<br>• HTTP 201 |
### 4.2 Обновление отдела (PATCH /api/v1/departments/{id}/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Изменения | Ожидаемый результат |
|----|----------|-----------|---------------------|
| D7 | Изменение имени | name="New Name" | • OU переименован в LDAP<br>• Обновлено в БД<br>• HTTP 200 |
| D8 | Смена главы | head_id=new_head | • set_head() в LDAP<br>• head_appointed_at установлен<br>• HTTP 200 |
| D9 | Удаление главы | head_id=null | • set_head(None) в LDAP<br>• head=null в БД<br>• HTTP 200 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Изменения | Ожидаемый результат |
|----|----------|-----------|---------------------|
| D10 | Изменение имени | name="New Name" | • Обновлено только в БД<br>• HTTP 200 |
| D11 | Смена главы | head_id=new_head | • head обновлён в БД<br>• head_appointed_at установлен<br>• HTTP 200 |
### 4.3 Управление участниками отдела
#### add_member (POST /api/v1/departments/{id}/add-member/)
##### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| D12 | Добавление участника | employee_id | • Пользователь MOVE в OU отдела в LDAP<br>• DepartmentMember создан/активирован<br>• HTTP 200 |
| D13 | Повторное добавление | Уже участник | • Активация существующей связи<br>• HTTP 200 |
| D14 | LDAP ошибка | LDAP недоступен | HTTP 502 |
##### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| D15 | Добавление участника | employee_id | • DepartmentMember создан/активирован в БД<br>• HTTP 200 |
#### remove_member (POST /api/v1/departments/{id}/remove-member/)
##### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| D16 | Удаление участника | employee_id | • Пользователь MOVE обратно в Users OU<br>• is_active=False в DepartmentMember<br>• HTTP 200 |
| D17 | Удаление несуществующего | Нет такого участника | HTTP 404 |
##### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| D18 | Удаление участника | employee_id | • is_active=False в БД<br>• 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 (группы)<br>• role обновлена в БД<br>• HTTP 200 |
| D20 | Снятие роли | employee_id, role_id=null | • Роли удалены в LDAP<br>• role=null в БД<br>• HTTP 200 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| D21 | Назначение роли | employee_id, role_id | • role обновлена только в БД<br>• HTTP 200 |
### 4.5 Удаление отдела (DELETE /api/v1/departments/{id}/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Условия | Ожидаемый результат |
|----|----------|---------|---------------------|
| D22 | Удаление пустого отдела | Нет участников | • OU удалён в LDAP<br>• Удалено из БД<br>• HTTP 204 |
| D23 | Удаление с участниками | Есть активные участники | • OU удалён<br>• Участники перемещены в Users OU<br>• HTTP 204 |
| D24 | LDAP ошибка без force_db | LDAP недоступен, force_db=false | HTTP 502 |
| D25 | LDAP ошибка с force_db | LDAP недоступен, force_db=true | • БД удалена несмотря на LDAP<br>• HTTP 204 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Условия | Ожидаемый результат |
|----|----------|---------|---------------------|
| D26 | Удаление отдела | Любые условия | • Удалено из БД<br>• HTTP 204 |
---
## 5. Тестирование EmployeeViewSet
### 5.1 Создание сотрудника (POST /api/v1/employees/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| E1 | Создание базовое | email, имя, пароль | • Пользователь в LDAP<br>• Запись в БД<br>• HTTP 201 |
| E2 | Создание с позицией | + position_id | • Создан в LDAP<br>• assign_position() вызван<br>• Группы позиции назначены<br>• HTTP 201 |
| E3 | Создание с аватаром | + avatar file | • Аватар в LDAP<br>• HTTP 201 |
| E4 | LDAP ошибка | LDAP недоступен | HTTP 502 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| E5 | Создание базовое | email, имя, пароль | • Только в БД<br>• Пароль через set_password()<br>• HTTP 201 |
| E6 | Создание с позицией | + position_id | • Создан в БД<br>• position установлена<br>• Группы через M2M<br>• HTTP 201 |
### 5.2 Обновление сотрудника (PATCH /api/v1/employees/{id}/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Изменения | Ожидаемый результат |
|----|----------|-----------|---------------------|
| E7 | Обновление имени | first_name, last_name | • update_user() в LDAP<br>• Обновлено в БД<br>• HTTP 200 |
| E8 | Смена email | email="new@mail.com" | • Email обновлён в LDAP<br>• Обновлён в БД<br>• HTTP 200 |
| E9 | Смена позиции | position_id=new_position | • Позиция обновлена в LDAP<br>• Группы пересчитаны<br>• HTTP 200 |
| E10 | Удаление позиции | position=null | • Позиция удалена в LDAP<br>• position=null в БД<br>• HTTP 200 |
| E11 | Обновление аватара | + новый avatar | • Аватар обновлён в LDAP<br>• Сохранён в БД<br>• HTTP 200 |
| E12 | Перемещение в отдел | department_dn=dept_dn | • MOVE в LDAP<br>• HTTP 200 |
| E13 | Назначение групп | group_cns=['CN=Group1',...] | • Группы назначены в LDAP<br>• HTTP 200 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Изменения | Ожидаемый результат |
|----|----------|-----------|---------------------|
| E14 | Обновление всех полей | first_name, last_name, email, phone, position, avatar | • Все обновлено в БД<br>• Аватар сохранён в media/<br>• HTTP 200 |
| E15 | Смена позиции | position_id=new_position | • Позиция обновлена в БД<br>• Группы через M2M<br>• 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/ | • Позиция в БД<br>• reconcile_position() вызван<br>• Группа создана в LDAP<br>• HTTP 201 |
| P2 | Обновление позиции | PATCH /api/v1/positions/{id}/ | • Обновлено в БД<br>• reconcile_position() вызван<br>• HTTP 200 |
| P3 | Удаление позиции | DELETE /api/v1/positions/{id}/ | • delete_position_group() вызван<br>• Удалено из БД<br>• HTTP 204 |
| P4 | Назначение групп | POST /positions/{id}/set-groups | • groups.set() в БД<br>• reconcile_position() синхронизирует LDAP<br>• HTTP 200 |
| P5 | Добавление групп | POST /positions/{id}/add-groups | • groups.add() в БД<br>• reconcile_position()<br>• HTTP 200 |
| P6 | Удаление групп | POST /positions/{id}/remove-groups | • groups.remove() в БД<br>• reconcile_position()<br>• HTTP 200 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Действие | Ожидаемый результат |
|----|----------|----------|---------------------|
| P7 | Создание позиции | POST /api/v1/positions/ | • Только в БД<br>• LDAP не вызывается<br>• HTTP 201 |
| P8 | Обновление позиции | PATCH /api/v1/positions/{id}/ | • Обновлено в БД<br>• HTTP 200 |
| P9 | Удаление позиции | DELETE /api/v1/positions/{id}/ | • Удалено из БД<br>• HTTP 204 |
| P10 | Управление группами | set/add/remove-groups | • Только M2M в БД<br>• 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<br>• Запись в БД<br>• HTTP 201 |
| G2 | С permissions | + permissions=[1,2,3] | • Группа в LDAP<br>• Permissions в БД<br>• HTTP 201 |
| G3 | LDAP ошибка | LDAP недоступен | HTTP 502 |
| G4 | Ошибка БД | БД сбой после LDAP | • Группа удалена из LDAP (rollback)<br>• HTTP 500 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G5 | Создание группы | name, description, permissions | • Только в БД<br>• Permissions через M2M<br>• HTTP 201 |
### 7.2 Обновление группы (PATCH /api/v1/groups/{id}/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Изменения | Ожидаемый результат |
|----|----------|-----------|---------------------|
| G6 | Переименование | name="New Name" | • group_rename() в LDAP<br>• Обновлено в БД<br>• HTTP 200 |
| G7 | Изменение описания | ldap_description="..." | • group_set_description() в LDAP<br>• HTTP 200 |
| G8 | Группа не найдена в LDAP | _resolve_group_dn() → None | HTTP 404 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Изменения | Ожидаемый результат |
|----|----------|-----------|---------------------|
| G9 | Переименование | name="New Name" | • Обновлено в БД<br>• HTTP 200 |
### 7.3 Action методы группы
#### rename (POST /api/v1/groups/{id}/rename/)
##### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G10 | Переименование | new_name="NewName" | • group_rename() в LDAP<br>• name обновлён в БД<br>• HTTP 200 |
| G11 | LDAP ошибка | LDAP недоступен | HTTP 502 |
##### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G12 | Переименование | new_name="NewName" | • Обновлено в БД<br>• HTTP 200 |
#### set_description (POST /api/v1/groups/{id}/set-description/)
##### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G13 | Установка описания | description="..." | • group_set_description() в LDAP<br>• HTTP 200 |
##### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G14 | Установка описания | description="..." | • Ничего не происходит (нет поля в БД)<br>• HTTP 200 |
#### members (GET /api/v1/groups/{id}/members/)
##### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Условия | Ожидаемый результат |
|----|----------|---------|---------------------|
| G15 | Получение участников | Группа существует | • group_list_members() из LDAP<br>• Сопоставление с БД через LdapSyncState<br>• HTTP 200 {"dns": [...], "employees": [...]} |
##### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Условия | Ожидаемый результат |
|----|----------|---------|---------------------|
| G16 | Получение участников | Группа существует | • Участники из БД (grp.user_set.all())<br>• 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<br>• user_set.add() в БД<br>• HTTP 200 |
| G18 | Добавление через ID | member_ids=[1,2,3] | • ID→DN преобразование<br>• Добавлены в LDAP и БД<br>• HTTP 200 |
| G19 | Ошибка БД | БД сбой после LDAP | • group_remove_members() (rollback)<br>• HTTP 500 |
##### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G20 | Добавление участников | member_ids=[1,2,3] | • user_set.add() в БД<br>• 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<br>• user_set.remove() в БД<br>• HTTP 200 |
| G22 | Ошибка БД | БД сбой после LDAP | • group_add_members() (rollback)<br>• HTTP 500 |
##### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G23 | Удаление участников | member_ids=[1,2,3] | • user_set.remove() в БД<br>• HTTP 200 |
#### replace_members (POST /api/v1/groups/{id}/replace-members/)
##### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G24 | Замена состава | member_dns/member_ids | • Сохранён старый состав<br>• group_replace_members() в LDAP<br>• user_set.set() в БД<br>• HTTP 200 |
| G25 | Ошибка БД | БД сбой после LDAP | • group_replace_members(prev_dns) (rollback)<br>• HTTP 500 |
##### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Данные | Ожидаемый результат |
|----|----------|--------|---------------------|
| G26 | Замена состава | member_ids=[...] | • user_set.set() в БД<br>• HTTP 200 |
### 7.4 Удаление группы (DELETE /api/v1/groups/{id}/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Параметры | Ожидаемый результат |
|----|----------|-----------|---------------------|
| G27 | Удаление группы | force_db=false | • group_delete() в LDAP<br>• Удалено из БД<br>• HTTP 204 |
| G28 | LDAP ошибка без force | force_db=false, LDAP недоступен | HTTP 502 |
| G29 | LDAP ошибка с force | force_db=true, LDAP недоступен | • Игнорируем LDAP<br>• Удалено из БД<br>• HTTP 204 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Параметры | Ожидаемый результат |
|----|----------|-----------|---------------------|
| G30 | Удаление группы | Любые | • Удалено из БД<br>• HTTP 204 |
### 7.5 Список групп (GET /api/v1/groups/)
#### С LDAP (LDAP_ENABLED=True)
| ID | Сценарий | Условия | Ожидаемый результат |
|----|----------|---------|---------------------|
| G31 | Получение списка | Любые | • sync_groups_catalog() вызван (throttled)<br>• Список из БД<br>• HTTP 200 |
#### Без LDAP (LDAP_ENABLED=False)
| ID | Сценарий | Условия | Ожидаемый результат |
|----|----------|---------|---------------------|
| G32 | Получение списка | Любые | • Список из БД<br>• 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<br>2. Отключить LDAP<br>3. Обновить пользователя | • Обновление работает только с БД<br>• Данные не теряются |
| B2 | No LDAP→LDAP | 1. Создать пользователя без LDAP<br>2. Включить LDAP<br>3. Обновить пользователя | • Обновление пытается синхронизировать с LDAP<br>• Может вернуть 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<br>• Без LDAP: время < 10s |
| P2 | Добавление 50 участников в группу | 50 member_ids | • Операция выполняется атомарно<br>• При ошибке - полный rollback |
| P3 | Создание 20 отделов | 20 departments | • С LDAP: все OU созданы<br>• Без 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. Создать итоговый отчёт

View File

@ -1,464 +0,0 @@
"""
Тесты прав доступа через должности (Position + Groups).
Проверяют, что:
1. Пользователь с должностью получает права из групп должности
2. PositionRoleBackend корректно добавляет права через get_all_permissions
3. has_perm работает для прав из должностей
4. Права наследуются при назначении/изменении должности
"""
import pytest
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from employees.models import Position
User = get_user_model()
pytestmark = pytest.mark.django_db
# ============================================================================
# Фикстуры (на уровне модуля, доступны всем тестам)
# ============================================================================
@pytest.fixture
def feed_content_type():
"""ContentType для приложения feed (для прав публикаций)."""
ct, _ = ContentType.objects.get_or_create(
app_label="feed", model="post"
)
return ct
@pytest.fixture
def documents_content_type():
"""ContentType для приложения documents."""
ct, _ = ContentType.objects.get_or_create(
app_label="documents", model="document"
)
return ct
@pytest.fixture
def group_with_publish_perm(feed_content_type):
"""Группа с правом публикации в feed."""
group = Group.objects.create(name="publishers")
# Ищем существующее право или создаём новое
perm = Permission.objects.filter(
codename="publish_company_post",
content_type=feed_content_type,
).first()
if not perm:
perm = Permission.objects.create(
codename="publish_company_post",
name="Can publish company post",
content_type=feed_content_type,
)
group.permissions.add(perm)
return group
@pytest.fixture
def group_with_document_perm(documents_content_type):
"""Группа с правом добавления документов."""
group = Group.objects.create(name="doc_creators")
# Ищем существующее право или используем defaults
perm = Permission.objects.filter(
codename="add_document",
content_type=documents_content_type,
).first()
if not perm:
perm = Permission.objects.create(
codename="add_document",
name="Can add document",
content_type=documents_content_type,
)
group.permissions.add(perm)
return group
@pytest.fixture
def engineer_position(group_with_publish_perm, group_with_document_perm):
"""Должность 'Инженер' с двумя группами прав."""
position = Position.objects.create(
name="Инженер",
description="Технический специалист",
)
position.groups.add(
group_with_publish_perm, group_with_document_perm
)
return position
@pytest.fixture
def manager_position(group_with_publish_perm):
"""Должность 'Менеджер' только с правом публикации."""
position = Position.objects.create(
name="Менеджер",
description="Управленец",
)
position.groups.add(group_with_publish_perm)
return position
@pytest.fixture
def basic_user(user_factory):
"""Обычный пользователь без должности и прав."""
return user_factory(
email="user@example.com",
staff=False,
superuser=False,
verified=True,
active=True,
)
# ============================================================================
# Тесты
# ============================================================================
class TestPositionPermissions:
"""Тесты прав доступа через должности."""
def test_user_without_position_has_no_permissions(self, basic_user):
"""Пользователь без должности не имеет прав."""
assert not basic_user.has_perm("feed.publish_company_post")
assert not basic_user.has_perm("documents.add_document")
# Проверяем через get_all_permissions
all_perms = basic_user.get_all_permissions()
assert "feed.publish_company_post" not in all_perms
assert "documents.add_document" not in all_perms
def test_user_with_position_gets_permissions(
self, basic_user, engineer_position
):
"""Пользователь с должностью получает права из групп должности."""
# Назначаем должность
basic_user.position = engineer_position
basic_user.save()
# Обновляем пользователя из БД (чтобы подтянуть связи)
basic_user.refresh_from_db()
# Проверяем has_perm
assert basic_user.has_perm("feed.publish_company_post"), \
"Должен быть доступ к публикации (из группы publishers)"
assert basic_user.has_perm("documents.add_document"), \
"Должен быть доступ к созданию документов (из группы doc_creators)"
# Проверяем через get_all_permissions
all_perms = basic_user.get_all_permissions()
assert "feed.publish_company_post" in all_perms, \
f"Право должно быть в all_perms. Текущие права: {all_perms}"
assert "documents.add_document" in all_perms, \
f"Право должно быть в all_perms. Текущие права: {all_perms}"
def test_position_change_updates_permissions(
self, basic_user, engineer_position, manager_position
):
"""При смене должности права обновляются."""
# Назначаем инженера (2 права)
basic_user.position = engineer_position
basic_user.save()
# Получаем fresh instance чтобы сбросить кэш прав
user_id = basic_user.id
basic_user = User.objects.get(id=user_id)
assert basic_user.has_perm("feed.publish_company_post")
assert basic_user.has_perm("documents.add_document")
# Меняем на менеджера (только 1 право)
basic_user.position = manager_position
basic_user.save()
# Снова получаем fresh instance
basic_user = User.objects.get(id=user_id)
assert basic_user.has_perm("feed.publish_company_post"), \
"Право на публикацию должно остаться"
assert not basic_user.has_perm("documents.add_document"), \
"Право на документы должно исчезнуть"
def test_remove_position_removes_permissions(
self, basic_user, engineer_position
):
"""При снятии должности права удаляются."""
# Назначаем должность
basic_user.position = engineer_position
basic_user.save()
# Получаем fresh instance
user_id = basic_user.id
basic_user = User.objects.get(id=user_id)
assert basic_user.has_perm("feed.publish_company_post")
assert basic_user.has_perm("documents.add_document")
# Снимаем должность
basic_user.position = None
basic_user.save()
# Снова fresh instance для сброса кэша
basic_user = User.objects.get(id=user_id)
assert not basic_user.has_perm("feed.publish_company_post"), \
"Права должны исчезнуть после снятия должности"
assert not basic_user.has_perm("documents.add_document"), \
"Права должны исчезнуть после снятия должности"
def test_multiple_users_with_same_position(
self, user_factory, engineer_position
):
"""Несколько пользователей с одной должностью получают
одинаковые права.
"""
user1 = user_factory(email="user1@example.com")
user2 = user_factory(email="user2@example.com")
user1.position = engineer_position
user2.position = engineer_position
user1.save()
user2.save()
user1.refresh_from_db()
user2.refresh_from_db()
# Оба должны иметь одинаковые права
for user in [user1, user2]:
assert user.has_perm("feed.publish_company_post")
assert user.has_perm("documents.add_document")
def test_superuser_has_all_permissions(
self, user_factory, engineer_position
):
"""is_superuser имеет все права независимо от должности."""
superuser = user_factory(email="admin@example.com", superuser=True)
# Без должности superuser имеет все права
assert superuser.has_perm("feed.publish_company_post")
assert superuser.has_perm("documents.add_document")
assert superuser.has_perm("any.random_permission")
# С должностью тоже все права
superuser.position = engineer_position
superuser.save()
superuser = User.objects.get(id=superuser.id)
assert superuser.has_perm("feed.publish_company_post")
assert superuser.has_perm("documents.add_document")
assert superuser.has_perm("any.random_permission")
def test_position_group_permissions_are_additive(
self, basic_user, feed_content_type
):
"""Если у должности несколько групп, права суммируются."""
# Создаём две группы с разными правами
group1 = Group.objects.create(name="group1")
perm1 = Permission.objects.filter(
codename="perm1",
content_type=feed_content_type,
).first()
if not perm1:
perm1 = Permission.objects.create(
codename="perm1",
name="Permission 1",
content_type=feed_content_type,
)
group1.permissions.add(perm1)
group2 = Group.objects.create(name="group2")
perm2 = Permission.objects.filter(
codename="perm2",
content_type=feed_content_type,
).first()
if not perm2:
perm2 = Permission.objects.create(
codename="perm2",
name="Permission 2",
content_type=feed_content_type,
)
group2.permissions.add(perm2)
# Создаём должность с обеими группами
position = Position.objects.create(name="Multi-group Position")
position.groups.add(group1, group2)
basic_user.position = position
basic_user.save()
basic_user.refresh_from_db()
# Пользователь должен иметь права из обеих групп
assert basic_user.has_perm("feed.perm1")
assert basic_user.has_perm("feed.perm2")
def test_get_all_permissions_includes_position_perms(
self, basic_user, engineer_position
):
"""get_all_permissions() включает права из должности."""
basic_user.position = engineer_position
basic_user.save()
basic_user.refresh_from_db()
all_perms = basic_user.get_all_permissions()
# Проверяем, что это set
assert isinstance(all_perms, set)
# Проверяем содержимое
assert "feed.publish_company_post" in all_perms
assert "documents.add_document" in all_perms
# Проверяем, что нет дубликатов (set гарантирует это)
perms_list = list(all_perms)
assert len(perms_list) == len(set(perms_list))
class TestPositionPermissionsEdgeCases:
"""Тесты граничных случаев и edge cases."""
@pytest.fixture
def empty_position(self):
"""Должность без групп."""
return Position.objects.create(
name="Empty Position",
description="Должность без прав",
)
def test_position_without_groups_grants_no_permissions(
self, user_factory, empty_position
):
"""Должность без групп не даёт прав."""
user = user_factory(email="user@example.com")
user.position = empty_position
user.save()
user.refresh_from_db()
all_perms = user.get_all_permissions()
assert len(all_perms) == 0, "Должность без групп не должна давать прав"
def test_inactive_user_has_no_permissions(
self, user_factory, engineer_position
):
"""Неактивный пользователь не имеет прав даже с должностью."""
user = user_factory(email="inactive@example.com", active=False)
user.position = engineer_position
user.save()
# Получаем fresh instance
user = User.objects.get(id=user.id)
# is_active=False блокирует все права
assert not user.has_perm("feed.publish_company_post")
assert not user.has_perm("documents.add_document")
def test_position_permissions_with_direct_user_permissions(
self, user_factory, engineer_position, feed_content_type
):
"""Права из должности + прямые права пользователя объединяются."""
user = user_factory(email="user@example.com")
# Даём пользователю прямое право
direct_perm, _ = Permission.objects.get_or_create(
codename="direct_perm",
name="Direct Permission",
content_type=feed_content_type,
)
user.user_permissions.add(direct_perm)
# Назначаем должность
user.position = engineer_position
user.save()
user.refresh_from_db()
# Должен иметь и прямые права, и права из должности
assert user.has_perm("feed.direct_perm"), \
"Прямое право должно работать"
assert user.has_perm("feed.publish_company_post"), \
"Право из должности должно работать"
assert user.has_perm("documents.add_document"), \
"Право из должности должно работать"
class TestPositionPermissionsIntegration:
"""Интеграционные тесты с реальными моделями приложения."""
def test_feed_permissions_through_position(
self, user_factory, feed_content_type
):
"""Проверка прав на публикацию постов через должность."""
# Создаём группу с правами публикации
publishers = Group.objects.create(name="publishers")
for codename in ["publish_company_post", "publish_department_post"]:
perm, _ = Permission.objects.get_or_create(
codename=codename,
name=f"Can {codename.replace('_', ' ')}",
content_type=feed_content_type,
)
publishers.permissions.add(perm)
# Создаём должность редактора
editor_position = Position.objects.create(name="Редактор")
editor_position.groups.add(publishers)
# Назначаем пользователю
user = user_factory(email="editor@example.com")
user.position = editor_position
user.save()
user.refresh_from_db()
# Проверяем права
assert user.has_perm("feed.publish_company_post")
assert user.has_perm("feed.publish_department_post")
def test_documents_permissions_through_position(
self, user_factory, documents_content_type
):
"""Проверка прав на документы через должность."""
# Создаём группу с правами на документы
doc_managers = Group.objects.create(name="doc_managers")
for codename in [
"add_document", "change_document",
"delete_document", "view_document"
]:
# Используем filter().first() вместо get_or_create
perm = Permission.objects.filter(
codename=codename,
content_type=documents_content_type,
).first()
if not perm:
perm = Permission.objects.create(
codename=codename,
name=f"Can {codename.replace('_', ' ')}",
content_type=documents_content_type,
)
doc_managers.permissions.add(perm)
# Создаём должность документоведа
doc_position = Position.objects.create(name="Документовед")
doc_position.groups.add(doc_managers)
# Назначаем пользователю
user = user_factory(email="docs@example.com")
user.position = doc_position
user.save()
user.refresh_from_db()
# Проверяем все CRUD права
assert user.has_perm("documents.add_document")
assert user.has_perm("documents.change_document")
assert user.has_perm("documents.delete_document")
assert user.has_perm("documents.view_document")

View File

@ -1,180 +0,0 @@
# tests/feed/test_models.py
import io
import pytest
from django.utils import timezone
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db import IntegrityError, transaction
from django.db.models.deletion import ProtectedError
from django.core.exceptions import ValidationError
from employees.models import Employee, Department
from feed.models import Post, Comment, PostLike
from feed.constants import TYPE_COMPANY, TYPE_DEPARTMENT, TYPE_EMPLOYEE
# ---------- helpers ----------
_seq = 1
def _uniq_email(prefix="u"):
global _seq
_seq += 1
return f"{prefix}{_seq}@example.com"
def _uniq_phone():
global _seq
_seq += 1
return f"+7999{_seq:07d}"
def _user(**kwargs) -> Employee:
u = Employee.objects.create_user(
email=_uniq_email(),
password="pass",
phone_number=_uniq_phone(),
send_activation_email=False,
first_name="T",
last_name="U",
)
if kwargs:
for k, v in kwargs.items():
setattr(u, k, v)
u.save()
return u
def _img_file(name="p.png"):
# простой валидный файл для ImageField
return SimpleUploadedFile(name, b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR", content_type="image/png")
def _bin_file(name="a.txt"):
return SimpleUploadedFile(name, b"hello", content_type="text/plain")
# ---------- Post ----------
@pytest.mark.django_db
def test_post_department_constraints():
author = _user()
dept = Department.objects.create(name="QA")
# type=department без department -> IntegrityError
with pytest.raises(IntegrityError):
with transaction.atomic():
Post.objects.create(author=author, type=TYPE_DEPARTMENT, title="t", body="b")
# type!=department с указанным department -> IntegrityError
with pytest.raises(IntegrityError):
with transaction.atomic():
Post.objects.create(author=author, type=TYPE_EMPLOYEE, department=dept, title="t", body="b")
# валидные кейсы
p1 = Post.objects.create(author=author, type=TYPE_DEPARTMENT, department=dept, title="t1", body="b1")
p2 = Post.objects.create(author=author, type=TYPE_EMPLOYEE, title="t2", body="b2")
p3 = Post.objects.create(author=author, type=TYPE_COMPANY, title="t3", body="b3")
assert p1.department_id == dept.id
assert p2.department_id is None
assert p3.department_id is None
@pytest.mark.django_db
def test_post_on_delete_protect_department():
author = _user()
dept = Department.objects.create(name="R&D")
Post.objects.create(author=author, type=TYPE_DEPARTMENT, department=dept, title="news", body="text")
# отдел нельзя удалить — PROTECT
with pytest.raises(ProtectedError):
dept.delete()
@pytest.mark.django_db
def test_post_likes_count_non_negative_constraint():
author = _user()
# Попытка создать пост с отрицательным счётчиком — нарушает CheckConstraint
with pytest.raises(IntegrityError):
Post.objects.create(author=author, type=TYPE_EMPLOYEE, title="bad", body="b", likes_count=-1)
@pytest.mark.django_db
def test_post_ordering_pinned_then_created_at():
author = _user()
p1 = Post.objects.create(author=author, type=TYPE_EMPLOYEE, title="1", body="b", pinned=False)
p2 = Post.objects.create(author=author, type=TYPE_EMPLOYEE, title="2", body="b", pinned=True)
p3 = Post.objects.create(author=author, type=TYPE_EMPLOYEE, title="3", body="b", pinned=False)
# задаём явные времена, чтобы проверить сортировку по created_at ↓
now = timezone.now()
Post.objects.filter(pk=p1.pk).update(created_at=now - timezone.timedelta(minutes=2))
Post.objects.filter(pk=p2.pk).update(created_at=now - timezone.timedelta(minutes=1))
Post.objects.filter(pk=p3.pk).update(created_at=now - timezone.timedelta(minutes=3))
ids = list(Post.objects.values_list("id", flat=True))
# Сначала закреплённые (pinned=True), внутри — по created_at ↓, затем обычные — тоже по created_at ↓
# Здесь: p2 (pinned), потом p1 (новее p3), потом p3
assert ids == [p2.id, p1.id, p3.id]
@pytest.mark.django_db
def test_post_str_variants():
author = _user()
dept = Department.objects.create(name="Ops")
p_company = Post.objects.create(author=author, type=TYPE_COMPANY, title="Company news", body="b")
p_dept = Post.objects.create(author=author, type=TYPE_DEPARTMENT, department=dept, title="Dept news", body="b")
p_emp = Post.objects.create(author=author, type=TYPE_EMPLOYEE, title="Personal", body="b")
assert str(p_company).startswith("[Компания]")
assert f"[Отдел: {dept}]" in str(p_dept)
assert "[Сотрудник:" in str(p_emp)
# ---------- PostLike ----------
@pytest.mark.django_db
def test_postlike_unique_per_user_and_post():
u = _user()
p = Post.objects.create(author=u, type=TYPE_EMPLOYEE, title="t", body="b")
PostLike.objects.create(post=p, user=u)
with pytest.raises(IntegrityError):
with transaction.atomic():
PostLike.objects.create(post=p, user=u)
# __str__
assert "❤" in str(PostLike.objects.first())
# ---------- Comment ----------
@pytest.mark.django_db
def test_comment_requires_text_or_file_or_image():
u = _user()
p = Post.objects.create(author=u, type=TYPE_EMPLOYEE, title="t", body="b")
# только текст -> ок
c1 = Comment.objects.create(post=p, author=u, text="hi")
assert c1.pk
# только изображение -> ок
c2 = Comment.objects.create(post=p, author=u, image=_img_file())
assert c2.pk and c2.text == ""
# только файл -> ок
c3 = Comment.objects.create(post=p, author=u, attachment=_bin_file())
assert c3.pk and c3.text == ""
# пусто всё -> нарушает CheckConstraint
with pytest.raises((IntegrityError, ValidationError)):
with transaction.atomic():
c = Comment(post=p, author=u, text="")
c.full_clean()
c.save()
@pytest.mark.django_db
def test_comment_ordering_by_created_at():
u = _user()
p = Post.objects.create(author=u, type=TYPE_EMPLOYEE, title="t", body="b")
c1 = Comment.objects.create(post=p, author=u, text="a")
c2 = Comment.objects.create(post=p, author=u, text="b")
# Переставим created_at, чтобы проверить ordering ASC
now = timezone.now()
Comment.objects.filter(pk=c1.pk).update(created_at=now - timezone.timedelta(minutes=2))
Comment.objects.filter(pk=c2.pk).update(created_at=now - timezone.timedelta(minutes=1))
ordered = list(p.comments.values_list("id", flat=True))
assert ordered == [c1.id, c2.id]

View File

@ -1,16 +0,0 @@
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 be55d16d
dn: cn=config
objectClass: olcGlobal
cn: config
olcArgsFile: /var/run/slapd/slapd.args
olcLogLevel: none
olcPidFile: /var/run/slapd/slapd.pid
olcToolThreads: 1
structuralObjectClass: olcGlobal
entryUUID: db0b225e-4dc9-1040-847c-a5a6e7d3c4b9
creatorsName: cn=config
createTimestamp: 20251104125934Z
entryCSN: 20251104125935.238907Z#000000#000#000000
modifiersName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
modifyTimestamp: 20251104125935Z

View File

@ -1,16 +0,0 @@
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 fb4cff89
dn: cn=module{0}
objectClass: olcModuleList
cn: module{0}
olcModulePath: /usr/lib/ldap
olcModuleLoad: {0}back_mdb
olcModuleLoad: {1}memberof
olcModuleLoad: {2}refint
structuralObjectClass: olcModuleList
entryUUID: db169af8-4dc9-1040-8484-a5a6e7d3c4b9
creatorsName: cn=admin,cn=config
createTimestamp: 20251104125934Z
entryCSN: 20251104125935.376471Z#000000#000#000000
modifiersName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
modifyTimestamp: 20251104125935Z

View File

@ -1,12 +0,0 @@
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 3bbcc361
dn: cn=schema
objectClass: olcSchemaConfig
cn: schema
structuralObjectClass: olcSchemaConfig
entryUUID: db0f8092-4dc9-1040-847f-a5a6e7d3c4b9
creatorsName: cn=admin,cn=config
createTimestamp: 20251104125934Z
entryCSN: 20251104125934.347330Z#000000#000#000000
modifiersName: cn=admin,cn=config
modifyTimestamp: 20251104125934Z

View File

@ -1,247 +0,0 @@
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 24226b3f
dn: cn={0}core
objectClass: olcSchemaConfig
cn: {0}core
olcAttributeTypes: {0}( 2.5.4.2 NAME 'knowledgeInformation' DESC 'RFC2256: k
nowledge information' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.
121.1.15{32768} )
olcAttributeTypes: {1}( 2.5.4.4 NAME ( 'sn' 'surname' ) DESC 'RFC2256: last
(family) name(s) for which the entity is known by' SUP name )
olcAttributeTypes: {2}( 2.5.4.5 NAME 'serialNumber' DESC 'RFC2256: serial nu
mber of the entity' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMat
ch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44{64} )
olcAttributeTypes: {3}( 2.5.4.6 NAME ( 'c' 'countryName' ) DESC 'RFC4519: tw
o-letter ISO-3166 country code' SUP name SYNTAX 1.3.6.1.4.1.1466.115.121.1.
11 SINGLE-VALUE )
olcAttributeTypes: {4}( 2.5.4.7 NAME ( 'l' 'localityName' ) DESC 'RFC2256: l
ocality which this object resides in' SUP name )
olcAttributeTypes: {5}( 2.5.4.8 NAME ( 'st' 'stateOrProvinceName' ) DESC 'RF
C2256: state or province which this object resides in' SUP name )
olcAttributeTypes: {6}( 2.5.4.9 NAME ( 'street' 'streetAddress' ) DESC 'RFC2
256: street address of this object' EQUALITY caseIgnoreMatch SUBSTR caseIgn
oreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )
olcAttributeTypes: {7}( 2.5.4.10 NAME ( 'o' 'organizationName' ) DESC 'RFC22
56: organization this object belongs to' SUP name )
olcAttributeTypes: {8}( 2.5.4.11 NAME ( 'ou' 'organizationalUnitName' ) DESC
'RFC2256: organizational unit this object belongs to' SUP name )
olcAttributeTypes: {9}( 2.5.4.12 NAME 'title' DESC 'RFC2256: title associate
d with the entity' SUP name )
olcAttributeTypes: {10}( 2.5.4.14 NAME 'searchGuide' DESC 'RFC2256: search g
uide, deprecated by enhancedSearchGuide' SYNTAX 1.3.6.1.4.1.1466.115.121.1.
25 )
olcAttributeTypes: {11}( 2.5.4.15 NAME 'businessCategory' DESC 'RFC2256: bus
iness category' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch S
YNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )
olcAttributeTypes: {12}( 2.5.4.16 NAME 'postalAddress' DESC 'RFC2256: postal
address' EQUALITY caseIgnoreListMatch SUBSTR caseIgnoreListSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )
olcAttributeTypes: {13}( 2.5.4.17 NAME 'postalCode' DESC 'RFC2256: postal co
de' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.
1.4.1.1466.115.121.1.15{40} )
olcAttributeTypes: {14}( 2.5.4.18 NAME 'postOfficeBox' DESC 'RFC2256: Post O
ffice Box' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX
1.3.6.1.4.1.1466.115.121.1.15{40} )
olcAttributeTypes: {15}( 2.5.4.19 NAME 'physicalDeliveryOfficeName' DESC 'RF
C2256: Physical Delivery Office Name' EQUALITY caseIgnoreMatch SUBSTR caseI
gnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )
olcAttributeTypes: {16}( 2.5.4.20 NAME 'telephoneNumber' DESC 'RFC2256: Tele
phone Number' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstring
sMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50{32} )
olcAttributeTypes: {17}( 2.5.4.21 NAME 'telexNumber' DESC 'RFC2256: Telex Nu
mber' SYNTAX 1.3.6.1.4.1.1466.115.121.1.52 )
olcAttributeTypes: {18}( 2.5.4.22 NAME 'teletexTerminalIdentifier' DESC 'RFC
2256: Teletex Terminal Identifier' SYNTAX 1.3.6.1.4.1.1466.115.121.1.51 )
olcAttributeTypes: {19}( 2.5.4.23 NAME ( 'facsimileTelephoneNumber' 'fax' )
DESC 'RFC2256: Facsimile (Fax) Telephone Number' SYNTAX 1.3.6.1.4.1.1466.11
5.121.1.22 )
olcAttributeTypes: {20}( 2.5.4.24 NAME 'x121Address' DESC 'RFC2256: X.121 Ad
dress' EQUALITY numericStringMatch SUBSTR numericStringSubstringsMatch SYNT
AX 1.3.6.1.4.1.1466.115.121.1.36{15} )
olcAttributeTypes: {21}( 2.5.4.25 NAME 'internationaliSDNNumber' DESC 'RFC22
56: international ISDN number' EQUALITY numericStringMatch SUBSTR numericSt
ringSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36{16} )
olcAttributeTypes: {22}( 2.5.4.26 NAME 'registeredAddress' DESC 'RFC2256: re
gistered postal address' SUP postalAddress SYNTAX 1.3.6.1.4.1.1466.115.121.
1.41 )
olcAttributeTypes: {23}( 2.5.4.27 NAME 'destinationIndicator' DESC 'RFC2256:
destination indicator' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstring
sMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44{128} )
olcAttributeTypes: {24}( 2.5.4.28 NAME 'preferredDeliveryMethod' DESC 'RFC22
56: preferred delivery method' SYNTAX 1.3.6.1.4.1.1466.115.121.1.14 SINGLE-
VALUE )
olcAttributeTypes: {25}( 2.5.4.29 NAME 'presentationAddress' DESC 'RFC2256:
presentation address' EQUALITY presentationAddressMatch SYNTAX 1.3.6.1.4.1.
1466.115.121.1.43 SINGLE-VALUE )
olcAttributeTypes: {26}( 2.5.4.30 NAME 'supportedApplicationContext' DESC 'R
FC2256: supported application context' EQUALITY objectIdentifierMatch SYNTA
X 1.3.6.1.4.1.1466.115.121.1.38 )
olcAttributeTypes: {27}( 2.5.4.31 NAME 'member' DESC 'RFC2256: member of a g
roup' SUP distinguishedName )
olcAttributeTypes: {28}( 2.5.4.32 NAME 'owner' DESC 'RFC2256: owner (of the
object)' SUP distinguishedName )
olcAttributeTypes: {29}( 2.5.4.33 NAME 'roleOccupant' DESC 'RFC2256: occupan
t of role' SUP distinguishedName )
olcAttributeTypes: {30}( 2.5.4.36 NAME 'userCertificate' DESC 'RFC2256: X.50
9 user certificate, use ;binary' EQUALITY certificateExactMatch SYNTAX 1.3.
6.1.4.1.1466.115.121.1.8 )
olcAttributeTypes: {31}( 2.5.4.37 NAME 'cACertificate' DESC 'RFC2256: X.509
CA certificate, use ;binary' EQUALITY certificateExactMatch SYNTAX 1.3.6.1.
4.1.1466.115.121.1.8 )
olcAttributeTypes: {32}( 2.5.4.38 NAME 'authorityRevocationList' DESC 'RFC22
56: X.509 authority revocation list, use ;binary' SYNTAX 1.3.6.1.4.1.1466.1
15.121.1.9 )
olcAttributeTypes: {33}( 2.5.4.39 NAME 'certificateRevocationList' DESC 'RFC
2256: X.509 certificate revocation list, use ;binary' SYNTAX 1.3.6.1.4.1.14
66.115.121.1.9 )
olcAttributeTypes: {34}( 2.5.4.40 NAME 'crossCertificatePair' DESC 'RFC2256:
X.509 cross certificate pair, use ;binary' SYNTAX 1.3.6.1.4.1.1466.115.121
.1.10 )
olcAttributeTypes: {35}( 2.5.4.42 NAME ( 'givenName' 'gn' ) DESC 'RFC2256: f
irst name(s) for which the entity is known by' SUP name )
olcAttributeTypes: {36}( 2.5.4.43 NAME 'initials' DESC 'RFC2256: initials of
some or all of names, but not the surname(s).' SUP name )
olcAttributeTypes: {37}( 2.5.4.44 NAME 'generationQualifier' DESC 'RFC2256:
name qualifier indicating a generation' SUP name )
olcAttributeTypes: {38}( 2.5.4.45 NAME 'x500UniqueIdentifier' DESC 'RFC2256:
X.500 unique identifier' EQUALITY bitStringMatch SYNTAX 1.3.6.1.4.1.1466.1
15.121.1.6 )
olcAttributeTypes: {39}( 2.5.4.46 NAME 'dnQualifier' DESC 'RFC2256: DN quali
fier' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR case
IgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 )
olcAttributeTypes: {40}( 2.5.4.47 NAME 'enhancedSearchGuide' DESC 'RFC2256:
enhanced search guide' SYNTAX 1.3.6.1.4.1.1466.115.121.1.21 )
olcAttributeTypes: {41}( 2.5.4.48 NAME 'protocolInformation' DESC 'RFC2256:
protocol information' EQUALITY protocolInformationMatch SYNTAX 1.3.6.1.4.1.
1466.115.121.1.42 )
olcAttributeTypes: {42}( 2.5.4.50 NAME 'uniqueMember' DESC 'RFC2256: unique
member of a group' EQUALITY uniqueMemberMatch SYNTAX 1.3.6.1.4.1.1466.115.1
21.1.34 )
olcAttributeTypes: {43}( 2.5.4.51 NAME 'houseIdentifier' DESC 'RFC2256: hous
e identifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYN
TAX 1.3.6.1.4.1.1466.115.121.1.15{32768} )
olcAttributeTypes: {44}( 2.5.4.52 NAME 'supportedAlgorithms' DESC 'RFC2256:
supported algorithms' SYNTAX 1.3.6.1.4.1.1466.115.121.1.49 )
olcAttributeTypes: {45}( 2.5.4.53 NAME 'deltaRevocationList' DESC 'RFC2256:
delta revocation list; use ;binary' SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 )
olcAttributeTypes: {46}( 2.5.4.54 NAME 'dmdName' DESC 'RFC2256: name of DMD'
SUP name )
olcAttributeTypes: {47}( 2.5.4.65 NAME 'pseudonym' DESC 'X.520(4th): pseudon
ym for the object' SUP name )
olcAttributeTypes: {48}( 0.9.2342.19200300.100.1.3 NAME ( 'mail' 'rfc822Mail
box' ) DESC 'RFC1274: RFC822 Mailbox' EQUALITY caseIgnoreIA5Match SUBSTR ca
seIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
olcAttributeTypes: {49}( 0.9.2342.19200300.100.1.25 NAME ( 'dc' 'domainCompo
nent' ) DESC 'RFC1274/2247: domain component' EQUALITY caseIgnoreIA5Match S
UBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SIN
GLE-VALUE )
olcAttributeTypes: {50}( 0.9.2342.19200300.100.1.37 NAME 'associatedDomain'
DESC 'RFC1274: domain associated with object' EQUALITY caseIgnoreIA5Match S
UBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {51}( 1.2.840.113549.1.9.1 NAME ( 'email' 'emailAddress'
'pkcs9email' ) DESC 'RFC3280: legacy attribute for email addresses in DNs'
EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.
6.1.4.1.1466.115.121.1.26{128} )
olcObjectClasses: {0}( 2.5.6.2 NAME 'country' DESC 'RFC2256: a country' SUP
top STRUCTURAL MUST c MAY ( searchGuide $ description ) )
olcObjectClasses: {1}( 2.5.6.3 NAME 'locality' DESC 'RFC2256: a locality' SU
P top STRUCTURAL MAY ( street $ seeAlso $ searchGuide $ st $ l $ descriptio
n ) )
olcObjectClasses: {2}( 2.5.6.4 NAME 'organization' DESC 'RFC2256: an organiz
ation' SUP top STRUCTURAL MUST o MAY ( userPassword $ searchGuide $ seeAlso
$ businessCategory $ x121Address $ registeredAddress $ destinationIndicato
r $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ tel
ephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street
$ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $
st $ l $ description ) )
olcObjectClasses: {3}( 2.5.6.5 NAME 'organizationalUnit' DESC 'RFC2256: an o
rganizational unit' SUP top STRUCTURAL MUST ou MAY ( userPassword $ searchG
uide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ desti
nationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalId
entifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNu
mber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDelive
ryOfficeName $ st $ l $ description ) )
olcObjectClasses: {4}( 2.5.6.6 NAME 'person' DESC 'RFC2256: a person' SUP to
p STRUCTURAL MUST ( sn $ cn ) MAY ( userPassword $ telephoneNumber $ seeAls
o $ description ) )
olcObjectClasses: {5}( 2.5.6.7 NAME 'organizationalPerson' DESC 'RFC2256: an
organizational person' SUP person STRUCTURAL MAY ( title $ x121Address $ r
egisteredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNu
mber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumbe
r $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postal
Address $ physicalDeliveryOfficeName $ ou $ st $ l ) )
olcObjectClasses: {6}( 2.5.6.8 NAME 'organizationalRole' DESC 'RFC2256: an o
rganizational role' SUP top STRUCTURAL MUST cn MAY ( x121Address $ register
edAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $
teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ fac
simileTelephoneNumber $ seeAlso $ roleOccupant $ preferredDeliveryMethod $
street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOffic
eName $ ou $ st $ l $ description ) )
olcObjectClasses: {7}( 2.5.6.9 NAME 'groupOfNames' DESC 'RFC2256: a group of
names (DNs)' SUP top STRUCTURAL MUST ( member $ cn ) MAY ( businessCategor
y $ seeAlso $ owner $ ou $ o $ description ) )
olcObjectClasses: {8}( 2.5.6.10 NAME 'residentialPerson' DESC 'RFC2256: an r
esidential person' SUP person STRUCTURAL MUST l MAY ( businessCategory $ x1
21Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMet
hod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internati
onaliSDNNumber $ facsimileTelephoneNumber $ preferredDeliveryMethod $ stree
t $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName
$ st $ l ) )
olcObjectClasses: {9}( 2.5.6.11 NAME 'applicationProcess' DESC 'RFC2256: an
application process' SUP top STRUCTURAL MUST cn MAY ( seeAlso $ ou $ l $ de
scription ) )
olcObjectClasses: {10}( 2.5.6.12 NAME 'applicationEntity' DESC 'RFC2256: an
application entity' SUP top STRUCTURAL MUST ( presentationAddress $ cn ) MA
Y ( supportedApplicationContext $ seeAlso $ ou $ o $ l $ description ) )
olcObjectClasses: {11}( 2.5.6.13 NAME 'dSA' DESC 'RFC2256: a directory syste
m agent (a server)' SUP applicationEntity STRUCTURAL MAY knowledgeInformati
on )
olcObjectClasses: {12}( 2.5.6.14 NAME 'device' DESC 'RFC2256: a device' SUP
top STRUCTURAL MUST cn MAY ( serialNumber $ seeAlso $ owner $ ou $ o $ l $
description ) )
olcObjectClasses: {13}( 2.5.6.15 NAME 'strongAuthenticationUser' DESC 'RFC22
56: a strong authentication user' SUP top AUXILIARY MUST userCertificate )
olcObjectClasses: {14}( 2.5.6.16 NAME 'certificationAuthority' DESC 'RFC2256
: a certificate authority' SUP top AUXILIARY MUST ( authorityRevocationList
$ certificateRevocationList $ cACertificate ) MAY crossCertificatePair )
olcObjectClasses: {15}( 2.5.6.17 NAME 'groupOfUniqueNames' DESC 'RFC2256: a
group of unique names (DN and Unique Identifier)' SUP top STRUCTURAL MUST (
uniqueMember $ cn ) MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ de
scription ) )
olcObjectClasses: {16}( 2.5.6.18 NAME 'userSecurityInformation' DESC 'RFC225
6: a user security information' SUP top AUXILIARY MAY supportedAlgorithms )
olcObjectClasses: {17}( 2.5.6.16.2 NAME 'certificationAuthority-V2' SUP cert
ificationAuthority AUXILIARY MAY deltaRevocationList )
olcObjectClasses: {18}( 2.5.6.19 NAME 'cRLDistributionPoint' SUP top STRUCTU
RAL MUST cn MAY ( certificateRevocationList $ authorityRevocationList $ del
taRevocationList ) )
olcObjectClasses: {19}( 2.5.6.20 NAME 'dmd' SUP top STRUCTURAL MUST dmdName
MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address
$ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ tel
exNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNN
umber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ po
stalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) )
olcObjectClasses: {20}( 2.5.6.21 NAME 'pkiUser' DESC 'RFC2587: a PKI user' S
UP top AUXILIARY MAY userCertificate )
olcObjectClasses: {21}( 2.5.6.22 NAME 'pkiCA' DESC 'RFC2587: PKI certificate
authority' SUP top AUXILIARY MAY ( authorityRevocationList $ certificateRe
vocationList $ cACertificate $ crossCertificatePair ) )
olcObjectClasses: {22}( 2.5.6.23 NAME 'deltaCRL' DESC 'RFC2587: PKI user' SU
P top AUXILIARY MAY deltaRevocationList )
olcObjectClasses: {23}( 1.3.6.1.4.1.250.3.15 NAME 'labeledURIObject' DESC 'R
FC2079: object that contains the URI attribute type' SUP top AUXILIARY MAY
labeledURI )
olcObjectClasses: {24}( 0.9.2342.19200300.100.4.19 NAME 'simpleSecurityObjec
t' DESC 'RFC1274: simple security object' SUP top AUXILIARY MUST userPasswo
rd )
olcObjectClasses: {25}( 1.3.6.1.4.1.1466.344 NAME 'dcObject' DESC 'RFC2247:
domain component object' SUP top AUXILIARY MUST dc )
olcObjectClasses: {26}( 1.3.6.1.1.3.1 NAME 'uidObject' DESC 'RFC2377: uid ob
ject' SUP top AUXILIARY MUST uid )
structuralObjectClass: olcSchemaConfig
entryUUID: db421a7a-4dc9-1040-8a4c-d9371890a28d
creatorsName: cn=config
createTimestamp: 20251104125934Z
entryCSN: 20251104125934.677931Z#000000#000#000000
modifiersName: cn=config
modifyTimestamp: 20251104125934Z

View File

@ -1,178 +0,0 @@
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 fcf296ee
dn: cn={1}cosine
objectClass: olcSchemaConfig
cn: {1}cosine
olcAttributeTypes: {0}( 0.9.2342.19200300.100.1.2 NAME 'textEncodedORAddress
' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.
4.1.1466.115.121.1.15{256} )
olcAttributeTypes: {1}( 0.9.2342.19200300.100.1.4 NAME 'info' DESC 'RFC1274:
general information' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsM
atch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{2048} )
olcAttributeTypes: {2}( 0.9.2342.19200300.100.1.5 NAME ( 'drink' 'favouriteD
rink' ) DESC 'RFC1274: favorite drink' EQUALITY caseIgnoreMatch SUBSTR case
IgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
olcAttributeTypes: {3}( 0.9.2342.19200300.100.1.6 NAME 'roomNumber' DESC 'RF
C1274: room number' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMat
ch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
olcAttributeTypes: {4}( 0.9.2342.19200300.100.1.7 NAME 'photo' DESC 'RFC1274
: photo (G3 fax)' SYNTAX 1.3.6.1.4.1.1466.115.121.1.23{25000} )
olcAttributeTypes: {5}( 0.9.2342.19200300.100.1.8 NAME 'userClass' DESC 'RFC
1274: category of user' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstring
sMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
olcAttributeTypes: {6}( 0.9.2342.19200300.100.1.9 NAME 'host' DESC 'RFC1274:
host computer' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch S
YNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
olcAttributeTypes: {7}( 0.9.2342.19200300.100.1.10 NAME 'manager' DESC 'RFC1
274: DN of manager' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466
.115.121.1.12 )
olcAttributeTypes: {8}( 0.9.2342.19200300.100.1.11 NAME 'documentIdentifier'
DESC 'RFC1274: unique identifier of document' EQUALITY caseIgnoreMatch SUB
STR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
olcAttributeTypes: {9}( 0.9.2342.19200300.100.1.12 NAME 'documentTitle' DESC
'RFC1274: title of document' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSub
stringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
olcAttributeTypes: {10}( 0.9.2342.19200300.100.1.13 NAME 'documentVersion' D
ESC 'RFC1274: version of document' EQUALITY caseIgnoreMatch SUBSTR caseIgno
reSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
olcAttributeTypes: {11}( 0.9.2342.19200300.100.1.14 NAME 'documentAuthor' DE
SC 'RFC1274: DN of author of document' EQUALITY distinguishedNameMatch SYNT
AX 1.3.6.1.4.1.1466.115.121.1.12 )
olcAttributeTypes: {12}( 0.9.2342.19200300.100.1.15 NAME 'documentLocation'
DESC 'RFC1274: location of document original' EQUALITY caseIgnoreMatch SUBS
TR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
olcAttributeTypes: {13}( 0.9.2342.19200300.100.1.20 NAME ( 'homePhone' 'home
TelephoneNumber' ) DESC 'RFC1274: home telephone number' EQUALITY telephone
NumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.1
15.121.1.50 )
olcAttributeTypes: {14}( 0.9.2342.19200300.100.1.21 NAME 'secretary' DESC 'R
FC1274: DN of secretary' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1
.1466.115.121.1.12 )
olcAttributeTypes: {15}( 0.9.2342.19200300.100.1.22 NAME 'otherMailbox' SYNT
AX 1.3.6.1.4.1.1466.115.121.1.39 )
olcAttributeTypes: {16}( 0.9.2342.19200300.100.1.26 NAME 'aRecord' EQUALITY
caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {17}( 0.9.2342.19200300.100.1.27 NAME 'mDRecord' EQUALITY
caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {18}( 0.9.2342.19200300.100.1.28 NAME 'mXRecord' EQUALITY
caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {19}( 0.9.2342.19200300.100.1.29 NAME 'nSRecord' EQUALITY
caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {20}( 0.9.2342.19200300.100.1.30 NAME 'sOARecord' EQUALIT
Y caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {21}( 0.9.2342.19200300.100.1.31 NAME 'cNAMERecord' EQUAL
ITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {22}( 0.9.2342.19200300.100.1.38 NAME 'associatedName' DE
SC 'RFC1274: DN of entry associated with domain' EQUALITY distinguishedName
Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 )
olcAttributeTypes: {23}( 0.9.2342.19200300.100.1.39 NAME 'homePostalAddress'
DESC 'RFC1274: home postal address' EQUALITY caseIgnoreListMatch SUBSTR ca
seIgnoreListSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 )
olcAttributeTypes: {24}( 0.9.2342.19200300.100.1.40 NAME 'personalTitle' DES
C 'RFC1274: personal title' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubst
ringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
olcAttributeTypes: {25}( 0.9.2342.19200300.100.1.41 NAME ( 'mobile' 'mobileT
elephoneNumber' ) DESC 'RFC1274: mobile telephone number' EQUALITY telephon
eNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.
115.121.1.50 )
olcAttributeTypes: {26}( 0.9.2342.19200300.100.1.42 NAME ( 'pager' 'pagerTel
ephoneNumber' ) DESC 'RFC1274: pager telephone number' EQUALITY telephoneNu
mberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115
.121.1.50 )
olcAttributeTypes: {27}( 0.9.2342.19200300.100.1.43 NAME ( 'co' 'friendlyCou
ntryName' ) DESC 'RFC1274: friendly country name' EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: {28}( 0.9.2342.19200300.100.1.44 NAME 'uniqueIdentifier'
DESC 'RFC1274: unique identifer' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.
1.1466.115.121.1.15{256} )
olcAttributeTypes: {29}( 0.9.2342.19200300.100.1.45 NAME 'organizationalStat
us' DESC 'RFC1274: organizational status' EQUALITY caseIgnoreMatch SUBSTR c
aseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
olcAttributeTypes: {30}( 0.9.2342.19200300.100.1.46 NAME 'janetMailbox' DESC
'RFC1274: Janet mailbox' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5S
ubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
olcAttributeTypes: {31}( 0.9.2342.19200300.100.1.47 NAME 'mailPreferenceOpti
on' DESC 'RFC1274: mail preference option' SYNTAX 1.3.6.1.4.1.1466.115.121.
1.27 )
olcAttributeTypes: {32}( 0.9.2342.19200300.100.1.48 NAME 'buildingName' DESC
'RFC1274: name of building' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubs
tringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
olcAttributeTypes: {33}( 0.9.2342.19200300.100.1.49 NAME 'dSAQuality' DESC '
RFC1274: DSA Quality' SYNTAX 1.3.6.1.4.1.1466.115.121.1.19 SINGLE-VALUE )
olcAttributeTypes: {34}( 0.9.2342.19200300.100.1.50 NAME 'singleLevelQuality
' DESC 'RFC1274: Single Level Quality' SYNTAX 1.3.6.1.4.1.1466.115.121.1.13
SINGLE-VALUE )
olcAttributeTypes: {35}( 0.9.2342.19200300.100.1.51 NAME 'subtreeMinimumQual
ity' DESC 'RFC1274: Subtree Mininum Quality' SYNTAX 1.3.6.1.4.1.1466.115.12
1.1.13 SINGLE-VALUE )
olcAttributeTypes: {36}( 0.9.2342.19200300.100.1.52 NAME 'subtreeMaximumQual
ity' DESC 'RFC1274: Subtree Maximun Quality' SYNTAX 1.3.6.1.4.1.1466.115.12
1.1.13 SINGLE-VALUE )
olcAttributeTypes: {37}( 0.9.2342.19200300.100.1.53 NAME 'personalSignature'
DESC 'RFC1274: Personal Signature (G3 fax)' SYNTAX 1.3.6.1.4.1.1466.115.12
1.1.23 )
olcAttributeTypes: {38}( 0.9.2342.19200300.100.1.54 NAME 'dITRedirect' DESC
'RFC1274: DIT Redirect' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.
1466.115.121.1.12 )
olcAttributeTypes: {39}( 0.9.2342.19200300.100.1.55 NAME 'audio' DESC 'RFC12
74: audio (u-law)' SYNTAX 1.3.6.1.4.1.1466.115.121.1.4{25000} )
olcAttributeTypes: {40}( 0.9.2342.19200300.100.1.56 NAME 'documentPublisher'
DESC 'RFC1274: publisher of document' EQUALITY caseIgnoreMatch SUBSTR case
IgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcObjectClasses: {0}( 0.9.2342.19200300.100.4.4 NAME ( 'pilotPerson' 'newPi
lotPerson' ) SUP person STRUCTURAL MAY ( userid $ textEncodedORAddress $ rf
c822Mailbox $ favouriteDrink $ roomNumber $ userClass $ homeTelephoneNumber
$ homePostalAddress $ secretary $ personalTitle $ preferredDeliveryMethod
$ businessCategory $ janetMailbox $ otherMailbox $ mobileTelephoneNumber $
pagerTelephoneNumber $ organizationalStatus $ mailPreferenceOption $ person
alSignature ) )
olcObjectClasses: {1}( 0.9.2342.19200300.100.4.5 NAME 'account' SUP top STRU
CTURAL MUST userid MAY ( description $ seeAlso $ localityName $ organizatio
nName $ organizationalUnitName $ host ) )
olcObjectClasses: {2}( 0.9.2342.19200300.100.4.6 NAME 'document' SUP top STR
UCTURAL MUST documentIdentifier MAY ( commonName $ description $ seeAlso $
localityName $ organizationName $ organizationalUnitName $ documentTitle $
documentVersion $ documentAuthor $ documentLocation $ documentPublisher ) )
olcObjectClasses: {3}( 0.9.2342.19200300.100.4.7 NAME 'room' SUP top STRUCTU
RAL MUST commonName MAY ( roomNumber $ description $ seeAlso $ telephoneNum
ber ) )
olcObjectClasses: {4}( 0.9.2342.19200300.100.4.9 NAME 'documentSeries' SUP t
op STRUCTURAL MUST commonName MAY ( description $ seeAlso $ telephonenumber
$ localityName $ organizationName $ organizationalUnitName ) )
olcObjectClasses: {5}( 0.9.2342.19200300.100.4.13 NAME 'domain' SUP top STRU
CTURAL MUST domainComponent MAY ( associatedName $ organizationName $ descr
iption $ businessCategory $ seeAlso $ searchGuide $ userPassword $ locality
Name $ stateOrProvinceName $ streetAddress $ physicalDeliveryOfficeName $ p
ostalAddress $ postalCode $ postOfficeBox $ streetAddress $ facsimileTeleph
oneNumber $ internationalISDNNumber $ telephoneNumber $ teletexTerminalIden
tifier $ telexNumber $ preferredDeliveryMethod $ destinationIndicator $ reg
isteredAddress $ x121Address ) )
olcObjectClasses: {6}( 0.9.2342.19200300.100.4.14 NAME 'RFC822localPart' SUP
domain STRUCTURAL MAY ( commonName $ surname $ description $ seeAlso $ tel
ephoneNumber $ physicalDeliveryOfficeName $ postalAddress $ postalCode $ po
stOfficeBox $ streetAddress $ facsimileTelephoneNumber $ internationalISDNN
umber $ telephoneNumber $ teletexTerminalIdentifier $ telexNumber $ preferr
edDeliveryMethod $ destinationIndicator $ registeredAddress $ x121Address )
)
olcObjectClasses: {7}( 0.9.2342.19200300.100.4.15 NAME 'dNSDomain' SUP domai
n STRUCTURAL MAY ( ARecord $ MDRecord $ MXRecord $ NSRecord $ SOARecord $ C
NAMERecord ) )
olcObjectClasses: {8}( 0.9.2342.19200300.100.4.17 NAME 'domainRelatedObject'
DESC 'RFC1274: an object related to an domain' SUP top AUXILIARY MUST asso
ciatedDomain )
olcObjectClasses: {9}( 0.9.2342.19200300.100.4.18 NAME 'friendlyCountry' SUP
country STRUCTURAL MUST friendlyCountryName )
olcObjectClasses: {10}( 0.9.2342.19200300.100.4.20 NAME 'pilotOrganization'
SUP ( organization $ organizationalUnit ) STRUCTURAL MAY buildingName )
olcObjectClasses: {11}( 0.9.2342.19200300.100.4.21 NAME 'pilotDSA' SUP dsa S
TRUCTURAL MAY dSAQuality )
olcObjectClasses: {12}( 0.9.2342.19200300.100.4.22 NAME 'qualityLabelledData
' SUP top AUXILIARY MUST dsaQuality MAY ( subtreeMinimumQuality $ subtreeMa
ximumQuality ) )
structuralObjectClass: olcSchemaConfig
entryUUID: db4230c8-4dc9-1040-8a4d-d9371890a28d
creatorsName: cn=config
createTimestamp: 20251104125934Z
entryCSN: 20251104125934.677931Z#000000#000#000000
modifiersName: cn=config
modifyTimestamp: 20251104125934Z

View File

@ -1,49 +0,0 @@
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 b7a0b8c6
dn: cn={2}inetorgperson
objectClass: olcSchemaConfig
cn: {2}inetorgperson
olcAttributeTypes: {0}( 2.16.840.1.113730.3.1.1 NAME 'carLicense' DESC 'RFC2
798: vehicle license or registration plate' EQUALITY caseIgnoreMatch SUBSTR
caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: {1}( 2.16.840.1.113730.3.1.2 NAME 'departmentNumber' DESC
'RFC2798: identifies a department within an organization' EQUALITY caseIgn
oreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1
.15 )
olcAttributeTypes: {2}( 2.16.840.1.113730.3.1.241 NAME 'displayName' DESC 'R
FC2798: preferred name to be used when displaying entries' EQUALITY caseIgn
oreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1
.15 SINGLE-VALUE )
olcAttributeTypes: {3}( 2.16.840.1.113730.3.1.3 NAME 'employeeNumber' DESC '
RFC2798: numerically identifies an employee within an organization' EQUALIT
Y caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.
115.121.1.15 SINGLE-VALUE )
olcAttributeTypes: {4}( 2.16.840.1.113730.3.1.4 NAME 'employeeType' DESC 'RF
C2798: type of employment for a person' EQUALITY caseIgnoreMatch SUBSTR cas
eIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: {5}( 0.9.2342.19200300.100.1.60 NAME 'jpegPhoto' DESC 'RF
C2798: a JPEG image' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 )
olcAttributeTypes: {6}( 2.16.840.1.113730.3.1.39 NAME 'preferredLanguage' DE
SC 'RFC2798: preferred written or spoken language for a person' EQUALITY ca
seIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.
121.1.15 SINGLE-VALUE )
olcAttributeTypes: {7}( 2.16.840.1.113730.3.1.40 NAME 'userSMIMECertificate'
DESC 'RFC2798: PKCS#7 SignedData used to support S/MIME' SYNTAX 1.3.6.1.4.
1.1466.115.121.1.5 )
olcAttributeTypes: {8}( 2.16.840.1.113730.3.1.216 NAME 'userPKCS12' DESC 'RF
C2798: personal identity information, a PKCS #12 PFX' SYNTAX 1.3.6.1.4.1.14
66.115.121.1.5 )
olcObjectClasses: {0}( 2.16.840.1.113730.3.2.2 NAME 'inetOrgPerson' DESC 'RF
C2798: Internet Organizational Person' SUP organizationalPerson STRUCTURAL
MAY ( audio $ businessCategory $ carLicense $ departmentNumber $ displayNam
e $ employeeNumber $ employeeType $ givenName $ homePhone $ homePostalAddre
ss $ initials $ jpegPhoto $ labeledURI $ mail $ manager $ mobile $ o $ page
r $ photo $ roomNumber $ secretary $ uid $ userCertificate $ x500uniqueIden
tifier $ preferredLanguage $ userSMIMECertificate $ userPKCS12 ) )
structuralObjectClass: olcSchemaConfig
entryUUID: db423690-4dc9-1040-8a4e-d9371890a28d
creatorsName: cn=config
createTimestamp: 20251104125934Z
entryCSN: 20251104125934.677931Z#000000#000#000000
modifiersName: cn=config
modifyTimestamp: 20251104125934Z

View File

@ -1,165 +0,0 @@
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 8d74a3ae
dn: cn={3}rfc2307bis
objectClass: olcSchemaConfig
cn: {3}rfc2307bis
olcAttributeTypes: {0}( 1.3.6.1.1.1.1.2 NAME 'gecos' DESC 'The GECOS field;
the common name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )
olcAttributeTypes: {1}( 1.3.6.1.1.1.1.3 NAME 'homeDirectory' DESC 'The absol
ute path to the home directory' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4
.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {2}( 1.3.6.1.1.1.1.4 NAME 'loginShell' DESC 'The path to
the login shell' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121
.1.26 SINGLE-VALUE )
olcAttributeTypes: {3}( 1.3.6.1.1.1.1.5 NAME 'shadowLastChange' EQUALITY int
egerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.2
7 SINGLE-VALUE )
olcAttributeTypes: {4}( 1.3.6.1.1.1.1.6 NAME 'shadowMin' EQUALITY integerMat
ch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGL
E-VALUE )
olcAttributeTypes: {5}( 1.3.6.1.1.1.1.7 NAME 'shadowMax' EQUALITY integerMat
ch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGL
E-VALUE )
olcAttributeTypes: {6}( 1.3.6.1.1.1.1.8 NAME 'shadowWarning' EQUALITY intege
rMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 S
INGLE-VALUE )
olcAttributeTypes: {7}( 1.3.6.1.1.1.1.9 NAME 'shadowInactive' EQUALITY integ
erMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
olcAttributeTypes: {8}( 1.3.6.1.1.1.1.10 NAME 'shadowExpire' EQUALITY intege
rMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 S
INGLE-VALUE )
olcAttributeTypes: {9}( 1.3.6.1.1.1.1.11 NAME 'shadowFlag' EQUALITY integerM
atch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SIN
GLE-VALUE )
olcAttributeTypes: {10}( 1.3.6.1.1.1.1.12 NAME 'memberUid' EQUALITY caseExac
tMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: {11}( 1.3.6.1.1.1.1.13 NAME 'memberNisNetgroup' EQUALITY
caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: {12}( 1.3.6.1.1.1.1.14 NAME 'nisNetgroupTriple' DESC 'Net
group triple' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYN
TAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: {13}( 1.3.6.1.1.1.1.15 NAME 'ipServicePort' DESC 'Service
port number' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.
3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {14}( 1.3.6.1.1.1.1.16 NAME 'ipServiceProtocol' DESC 'Ser
vice protocol name' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.12
1.1.15 )
olcAttributeTypes: {15}( 1.3.6.1.1.1.1.17 NAME 'ipProtocolNumber' DESC 'IP p
rotocol number' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX
1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {16}( 1.3.6.1.1.1.1.18 NAME 'oncRpcNumber' DESC 'ONC RPC
number' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.
4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {17}( 1.3.6.1.1.1.1.19 NAME 'ipHostNumber' DESC 'IPv4 add
resses as a dotted decimal omitting leading zeros or IPv6 add
resses as defined in RFC2373' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.
1.1466.115.121.1.26 )
olcAttributeTypes: {18}( 1.3.6.1.1.1.1.20 NAME 'ipNetworkNumber' DESC 'IP ne
twork omitting leading zeros, eg. 192.168' EQUALITY caseIgnoreIA5Match SYNT
AX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {19}( 1.3.6.1.1.1.1.21 NAME 'ipNetmaskNumber' DESC 'IP ne
tmask omitting leading zeros, eg. 255.255.255.0' EQUALITY caseIgnoreIA5Matc
h SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {20}( 1.3.6.1.1.1.1.22 NAME 'macAddress' DESC 'MAC addres
s in maximal, colon separated hex notation, eg. 00:00:92:90:e
e:e2' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {21}( 1.3.6.1.1.1.1.23 NAME 'bootParameter' DESC 'rpc.boo
tparamd parameter' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.1
21.1.26 )
olcAttributeTypes: {22}( 1.3.6.1.1.1.1.24 NAME 'bootFile' DESC 'Boot image n
ame' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {23}( 1.3.6.1.1.1.1.26 NAME 'nisMapName' DESC 'Name of a
generic NIS map' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1
.15{64} )
olcAttributeTypes: {24}( 1.3.6.1.1.1.1.27 NAME 'nisMapEntry' DESC 'A generic
NIS entry' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{10
24} SINGLE-VALUE )
olcAttributeTypes: {25}( 1.3.6.1.1.1.1.28 NAME 'nisPublicKey' DESC 'NIS publ
ic key' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 SING
LE-VALUE )
olcAttributeTypes: {26}( 1.3.6.1.1.1.1.29 NAME 'nisSecretKey' DESC 'NIS secr
et key' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 SING
LE-VALUE )
olcAttributeTypes: {27}( 1.3.6.1.1.1.1.30 NAME 'nisDomain' DESC 'NIS domain'
EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
olcAttributeTypes: {28}( 1.3.6.1.1.1.1.31 NAME 'automountMapName' DESC 'auto
mount Map Name' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.1
5 SINGLE-VALUE )
olcAttributeTypes: {29}( 1.3.6.1.1.1.1.32 NAME 'automountKey' DESC 'Automoun
t Key value' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 S
INGLE-VALUE )
olcAttributeTypes: {30}( 1.3.6.1.1.1.1.33 NAME 'automountInformation' DESC '
Automount information' EQUALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.
121.1.15 SINGLE-VALUE )
olcObjectClasses: {0}( 1.3.6.1.1.1.2.0 NAME 'posixAccount' DESC 'Abstraction
of an account with POSIX attributes' SUP top AUXILIARY MUST ( cn $ uid $ u
idNumber $ gidNumber $ homeDirectory ) MAY ( userPassword $ loginShell $ ge
cos $ description ) )
olcObjectClasses: {1}( 1.3.6.1.1.1.2.1 NAME 'shadowAccount' DESC 'Additional
attributes for shadow passwords' SUP top AUXILIARY MUST uid MAY ( userPass
word $ description $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarni
ng $ shadowInactive $ shadowExpire $ shadowFlag ) )
olcObjectClasses: {2}( 1.3.6.1.1.1.2.2 NAME 'posixGroup' DESC 'Abstraction o
f a group of accounts' SUP top AUXILIARY MUST gidNumber MAY ( userPassword
$ memberUid $ description ) )
olcObjectClasses: {3}( 1.3.6.1.1.1.2.3 NAME 'ipService' DESC 'Abstraction an
Internet Protocol service. Maps an IP port and protocol (suc
h as tcp or udp) to one or more names; the distinguished valu
e of the cn attribute denotes the services canonical
name' SUP top STRUCTURAL MUST ( cn $ ipServicePort $ ipServiceProtoco
l ) MAY description )
olcObjectClasses: {4}( 1.3.6.1.1.1.2.4 NAME 'ipProtocol' DESC 'Abstraction o
f an IP protocol. Maps a protocol number to one or more names
. The distinguished value of the cn attribute denotes the pro
tocol canonical name' SUP top STRUCTURAL MUST ( cn $ ipProtocolNumber ) MAY
description )
olcObjectClasses: {5}( 1.3.6.1.1.1.2.5 NAME 'oncRpc' DESC 'Abstraction of an
Open Network Computing (ONC) [RFC1057] Remote Procedure Call
(RPC) binding. This class maps an ONC RPC number to a name.
The distinguished value of the cn attribute denotes
the RPC service canonical name' SUP top STRUCTURAL MUST ( cn $ oncRpcNumbe
r ) MAY description )
olcObjectClasses: {6}( 1.3.6.1.1.1.2.6 NAME 'ipHost' DESC 'Abstraction of a
host, an IP device. The distinguished value of the cn attribu
te denotes the hosts canonical name. Device SHOULD be used as a
structural class' SUP top AUXILIARY MUST ( cn $ ipHostNumber ) MAY ( userPa
ssword $ l $ description $ manager ) )
olcObjectClasses: {7}( 1.3.6.1.1.1.2.7 NAME 'ipNetwork' DESC 'Abstraction of
a network. The distinguished value of the cn attribute denot
es the network canonical name' SUP top STRUCTURAL MUST ipNetworkNumber MAY
( cn $ ipNetmaskNumber $ l $ description $ manager ) )
olcObjectClasses: {8}( 1.3.6.1.1.1.2.8 NAME 'nisNetgroup' DESC 'Abstraction
of a netgroup. May refer to other netgroups' SUP top STRUCTUR
AL MUST cn MAY ( nisNetgroupTriple $ memberNisNetgroup $ description ) )
olcObjectClasses: {9}( 1.3.6.1.1.1.2.9 NAME 'nisMap' DESC 'A generic abstrac
tion of a NIS map' SUP top STRUCTURAL MUST nisMapName MAY description )
olcObjectClasses: {10}( 1.3.6.1.1.1.2.10 NAME 'nisObject' DESC 'An entry in
a NIS map' SUP top STRUCTURAL MUST ( cn $ nisMapEntry $ nisMapName ) )
olcObjectClasses: {11}( 1.3.6.1.1.1.2.11 NAME 'ieee802Device' DESC 'A device
with a MAC address; device SHOULD be used as a structural cl
ass' SUP top AUXILIARY MAY macAddress )
olcObjectClasses: {12}( 1.3.6.1.1.1.2.12 NAME 'bootableDevice' DESC 'A devic
e with boot parameters; device SHOULD be used as a structural
class' SUP top AUXILIARY MAY ( bootFile $ bootParameter ) )
olcObjectClasses: {13}( 1.3.6.1.1.1.2.14 NAME 'nisKeyObject' DESC 'An object
with a public and secret key' SUP top AUXILIARY MUST ( cn $ nisPublicKey $
nisSecretKey ) MAY ( uidNumber $ description ) )
olcObjectClasses: {14}( 1.3.6.1.1.1.2.15 NAME 'nisDomainObject' DESC 'Associ
ates a NIS domain with a naming context' SUP top AUXILIARY MUST nisDomain )
olcObjectClasses: {15}( 1.3.6.1.1.1.2.16 NAME 'automountMap' SUP top STRUCTU
RAL MUST automountMapName MAY description )
olcObjectClasses: {16}( 1.3.6.1.1.1.2.17 NAME 'automount' DESC 'Automount in
formation' SUP top STRUCTURAL MUST ( automountKey $ automountInformation )
MAY description )
olcObjectClasses: {17}( 1.3.6.1.1.1.2.18 NAME 'groupOfMembers' DESC 'A group
with members (DNs)' SUP top STRUCTURAL MUST cn MAY ( businessCategory $ se
eAlso $ owner $ ou $ o $ description $ member ) )
structuralObjectClass: olcSchemaConfig
entryUUID: db423c44-4dc9-1040-8a4f-d9371890a28d
creatorsName: cn=config
createTimestamp: 20251104125934Z
entryCSN: 20251104125934.677931Z#000000#000#000000
modifiersName: cn=config
modifyTimestamp: 20251104125934Z

View File

@ -1,65 +0,0 @@
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 cbb09157
dn: cn={4}ppolicy
objectClass: olcSchemaConfig
cn: {4}ppolicy
olcAttributeTypes: {0}( 1.3.6.1.4.1.42.2.27.8.1.1 NAME 'pwdAttribute' EQUALI
TY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )
olcAttributeTypes: {1}( 1.3.6.1.4.1.42.2.27.8.1.2 NAME 'pwdMinAge' EQUALITY
integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.
1.27 SINGLE-VALUE )
olcAttributeTypes: {2}( 1.3.6.1.4.1.42.2.27.8.1.3 NAME 'pwdMaxAge' EQUALITY
integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.
1.27 SINGLE-VALUE )
olcAttributeTypes: {3}( 1.3.6.1.4.1.42.2.27.8.1.4 NAME 'pwdInHistory' EQUALI
TY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.1
21.1.27 SINGLE-VALUE )
olcAttributeTypes: {4}( 1.3.6.1.4.1.42.2.27.8.1.5 NAME 'pwdCheckQuality' EQU
ALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.11
5.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {5}( 1.3.6.1.4.1.42.2.27.8.1.6 NAME 'pwdMinLength' EQUALI
TY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.
121.1.27 SINGLE-VALUE )
olcAttributeTypes: {6}( 1.3.6.1.4.1.42.2.27.8.1.7 NAME 'pwdExpireWarning' EQ
UALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.
115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {7}( 1.3.6.1.4.1.42.2.27.8.1.8 NAME 'pwdGraceAuthNLimit'
EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.146
6.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {8}( 1.3.6.1.4.1.42.2.27.8.1.9 NAME 'pwdLockout' EQUALITY
booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
olcAttributeTypes: {9}( 1.3.6.1.4.1.42.2.27.8.1.10 NAME 'pwdLockoutDuration'
EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.14
66.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {10}( 1.3.6.1.4.1.42.2.27.8.1.11 NAME 'pwdMaxFailure' EQU
ALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.1
15.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {11}( 1.3.6.1.4.1.42.2.27.8.1.12 NAME 'pwdFailureCountInt
erval' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.
4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {12}( 1.3.6.1.4.1.42.2.27.8.1.13 NAME 'pwdMustChange' EQU
ALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
olcAttributeTypes: {13}( 1.3.6.1.4.1.42.2.27.8.1.14 NAME 'pwdAllowUserChange
' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
olcAttributeTypes: {14}( 1.3.6.1.4.1.42.2.27.8.1.15 NAME 'pwdSafeModify' EQU
ALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
olcAttributeTypes: {15}( 1.3.6.1.4.1.4754.1.99.1 NAME 'pwdCheckModule' DESC
'Loadable module that instantiates "check_password() function' EQUALITY cas
eExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {16}( 1.3.6.1.4.1.42.2.27.8.1.30 NAME 'pwdMaxRecordedFail
ure' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.
1.1466.115.121.1.27 SINGLE-VALUE )
olcObjectClasses: {0}( 1.3.6.1.4.1.4754.2.99.1 NAME 'pwdPolicyChecker' SUP t
op AUXILIARY MAY pwdCheckModule )
olcObjectClasses: {1}( 1.3.6.1.4.1.42.2.27.8.2.1 NAME 'pwdPolicy' SUP top AU
XILIARY MUST pwdAttribute MAY ( pwdMinAge $ pwdMaxAge $ pwdInHistory $ pwdC
heckQuality $ pwdMinLength $ pwdExpireWarning $ pwdGraceAuthNLimit $ pwdLoc
kout $ pwdLockoutDuration $ pwdMaxFailure $ pwdFailureCountInterval $ pwdMu
stChange $ pwdAllowUserChange $ pwdSafeModify $ pwdMaxRecordedFailure ) )
structuralObjectClass: olcSchemaConfig
entryUUID: db5df02e-4dc9-1040-9b56-b713c26c3953
creatorsName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
createTimestamp: 20251104125934Z
entryCSN: 20251104125934.861372Z#000000#000#000000
modifiersName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
modifyTimestamp: 20251104125934Z

View File

@ -1,175 +0,0 @@
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 3ea1f2eb
dn: cn={5}kopano
objectClass: olcSchemaConfig
cn: {5}kopano
olcAttributeTypes: {0}( 1.3.6.1.4.1.47732.1.1.1.1 NAME 'kopanoQuotaOverride'
DESC 'KOPANO: Override child quota' EQUALITY integerMatch SYNTAX 1.3.6.1.4
.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {1}( 1.3.6.1.4.1.47732.1.1.1.2 NAME 'kopanoQuotaWarn' DES
C 'KOPANO: Warning quota size in MB' EQUALITY integerMatch SYNTAX 1.3.6.1.4
.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {2}( 1.3.6.1.4.1.47732.1.1.1.3 NAME 'kopanoQuotaSoft' DES
C 'KOPANO: Soft quota size in MB' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.
1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {3}( 1.3.6.1.4.1.47732.1.1.1.4 NAME 'kopanoQuotaHard' DES
C 'KOPANO: Hard quota size in MB' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.
1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {4}( 1.3.6.1.4.1.47732.1.1.1.5 NAME 'kopanoUserDefaultQuo
taOverride' DESC 'KOPANO: Override User default quota for children' EQUALIT
Y integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {5}( 1.3.6.1.4.1.47732.1.1.1.6 NAME 'kopanoUserDefaultQuo
taWarn' DESC 'KOPANO: User default warning quota size in MB' EQUALITY integ
erMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {6}( 1.3.6.1.4.1.47732.1.1.1.7 NAME 'kopanoUserDefaultQuo
taSoft' DESC 'KOPANO: User default soft quota size in MB' EQUALITY integerM
atch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {7}( 1.3.6.1.4.1.47732.1.1.1.8 NAME 'kopanoUserDefaultQuo
taHard' DESC 'KOPANO: User default hard quota size in MB' EQUALITY integerM
atch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {8}( 1.3.6.1.4.1.47732.1.1.2.1 NAME 'kopanoAdmin' DESC 'K
OPANO: Administrator of kopano' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.14
66.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {9}( 1.3.6.1.4.1.47732.1.1.2.2 NAME 'kopanoSharedStoreOnl
y' DESC 'KOPANO: is store a shared store' EQUALITY integerMatch SYNTAX 1.3.
6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {10}( 1.3.6.1.4.1.47732.1.1.2.3 NAME 'kopanoAccount' DESC
'KOPANO: entry is a part of kopano' EQUALITY integerMatch SYNTAX 1.3.6.1.4
.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {11}( 1.3.6.1.4.1.47732.1.1.2.4 NAME 'kopanoSendAsPrivile
ge' DESC 'KOPANO: Users may directly send email as this user' EQUALITY case
IgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.12
1.1.15 )
olcAttributeTypes: {12}( 1.3.6.1.4.1.47732.1.1.2.5 NAME 'kopanoMrAccept' DES
C 'KOPANO: user should auto-accept meeting requests' EQUALITY integerMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {13}( 1.3.6.1.4.1.47732.1.1.2.6 NAME 'kopanoMrDeclineConf
lict' DESC 'KOPANO: user should automatically decline conflicting meeting r
equests' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-
VALUE )
olcAttributeTypes: {14}( 1.3.6.1.4.1.47732.1.1.2.7 NAME 'kopanoMrDeclineRecu
rring' DESC 'KOPANO: user should automatically decline recurring meeting re
quests' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-V
ALUE )
olcAttributeTypes: {15}( 1.3.6.1.4.1.47732.1.1.2.8 NAME 'kopanoId' DESC 'KOP
ANO: Generic unique ID' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.1
15.121.1.40 SINGLE-VALUE )
olcAttributeTypes: {16}( 1.3.6.1.4.1.47732.1.1.2.9 NAME 'kopanoResourceType'
DESC 'KOPANO: for shared stores, resource is type Room or Equipment' EQUAL
ITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.146
6.115.121.1.15 SINGLE-VALUE )
olcAttributeTypes: {17}( 1.3.6.1.4.1.47732.1.1.2.10 NAME 'kopanoResourceCapa
city' DESC 'KOPANO: number of rooms or equipment available' EQUALITY intege
rMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {18}( 1.3.6.1.4.1.47732.1.1.2.11 NAME 'kopanoHidden' DESC
'KOPANO: This object should be hidden from address book' EQUALITY integerM
atch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {19}( 1.3.6.1.4.1.47732.1.1.2.13 NAME 'kopanoEnabledFeatu
res' DESC 'KOPANO: This user has these features explicitly enabled' EQUALIT
Y caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 )
olcAttributeTypes: {20}( 1.3.6.1.4.1.47732.1.1.2.14 NAME 'kopanoDisabledFeat
ures' DESC 'KOPANO: This user has these features explicitly disabled' EQUAL
ITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 )
olcAttributeTypes: {21}( 1.3.6.1.4.1.47732.1.1.3.1 NAME 'kopanoAliases' DESC
'KOPANO: All other email addresses for this user' EQUALITY caseIgnoreMatch
SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: {22}( 1.3.6.1.4.1.47732.1.1.4.1 NAME 'kopanoUserServer' D
ESC 'KOPANO: Home server for the user' EQUALITY caseIgnoreMatch SUBSTR case
IgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )
olcAttributeTypes: {23}( 1.3.6.1.4.1.47732.1.1.6.1 NAME 'kopanoUserArchiveSe
rvers' DESC 'KOPANO: List of server names that contain an archive store for
the user' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX
1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: {24}( 1.3.6.1.4.1.47732.1.1.6.2 NAME 'kopanoUserArchiveCo
uplings' DESC 'KOPANO: List of username:foldername pairs that specify many-
to-one archive locations' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstri
ngsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: {25}( 1.3.6.1.4.1.47732.1.2.2.1 NAME 'kopanoSecurityGroup
' DESC 'KOPANO: group has security possibilities' EQUALITY integerMatch SYN
TAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {26}( 1.3.6.1.4.1.47732.1.3.2.4 NAME 'kopanoViewPrivilege
' DESC 'KOPANO: Companies with view privileges over selected company' EQUAL
ITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.146
6.115.121.1.15 )
olcAttributeTypes: {27}( 1.3.6.1.4.1.47732.1.3.2.5 NAME 'kopanoAdminPrivileg
e' DESC 'KOPANO: Users from different companies which are administrator ove
r selected company' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMat
ch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: {28}( 1.3.6.1.4.1.47732.1.3.2.6 NAME 'kopanoSystemAdmin'
DESC 'KOPANO: The user who is the system administrator for this company' EQ
UALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.
1466.115.121.1.15 SINGLE-VALUE )
olcAttributeTypes: {29}( 1.3.6.1.4.1.47732.1.3.1.5 NAME 'kopanoQuotaUserWarn
ingRecipients' DESC 'KOPANO: Users who will receive a notification email wh
en a user exceeds his quota' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubs
tringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: {30}( 1.3.6.1.4.1.47732.1.3.1.6 NAME 'kopanoQuotaCompanyW
arningRecipients' DESC 'KOPANO: Users who will receive a notification email
when a company exceeds its quota' EQUALITY caseIgnoreMatch SUBSTR caseIgno
reSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
olcAttributeTypes: {31}( 1.3.6.1.4.1.47732.1.3.4.1 NAME 'kopanoCompanyServer
' DESC 'KOPANO: Home server for the public folders for a company' EQUALITY
caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.11
5.121.1.15 SINGLE-VALUE )
olcAttributeTypes: {32}( 1.3.6.1.4.1.47732.1.4.4.1 NAME 'kopanoHttpPort' DES
C 'KOPANO: Port for the http connection' EQUALITY integerMatch SYNTAX 1.3.6
.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {33}( 1.3.6.1.4.1.47732.1.4.4.2 NAME 'kopanoSslPort' DESC
'KOPANO: Port for the ssl connection' EQUALITY integerMatch SYNTAX 1.3.6.1
.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {34}( 1.3.6.1.4.1.47732.1.4.4.3 NAME 'kopanoFilePath' DES
C 'KOPANO: The Unix socket or named pipe to the server' EQUALITY caseIgnore
Match SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
olcAttributeTypes: {35}( 1.3.6.1.4.1.47732.1.4.4.4 NAME 'kopanoContainsPubli
c' DESC 'KOPANO: This server contains the public store' EQUALITY integerMat
ch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {36}( 1.3.6.1.4.1.47732.1.4.4.6 NAME 'kopanoProxyURL' DES
C 'KOPANO: Full proxy URL for this server' EQUALITY caseIgnoreMatch SUBSTR
caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE
)
olcAttributeTypes: {37}( 1.3.6.1.4.1.47732.1.5.5.1 NAME 'kopanoFilter' DESC
'KOPANO: LDAP Filter to apply' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSu
bstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )
olcAttributeTypes: {38}( 1.3.6.1.4.1.47732.1.5.5.2 NAME 'kopanoBase' DESC 'K
OPANO: LDAP Search base to apply' EQUALITY caseIgnoreMatch SUBSTR caseIgnor
eSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE )
olcObjectClasses: {0}( 1.3.6.1.4.1.47732.1.1.0.0 NAME 'kopano-user' DESC 'KO
PANO: an user of Kopano' SUP top AUXILIARY MUST cn MAY ( kopanoQuotaOverrid
e $ kopanoQuotaWarn $ kopanoQuotaSoft $ kopanoSendAsPrivilege $ kopanoQuota
Hard $ kopanoAdmin $ kopanoSharedStoreOnly $ kopanoResourceType $ kopanoRes
ourceCapacity $ kopanoAccount $ kopanoHidden $ kopanoAliases $ kopanoUserSe
rver $ kopanoEnabledFeatures $ kopanoDisabledFeatures $ kopanoUserArchiveSe
rvers $ kopanoUserArchiveCouplings $ uidNumber ) )
olcObjectClasses: {1}( 1.3.6.1.4.1.47732.1.6.0.0 NAME 'kopano-contact' DESC
'KOPANO: a contact of Kopano' SUP top AUXILIARY MUST ( cn $ uidNumber ) MAY
( kopanoSendAsPrivilege $ kopanoHidden $ kopanoAliases $ kopanoAccount ) )
olcObjectClasses: {2}( 1.3.6.1.4.1.47732.1.2.0.0 NAME 'kopano-group' DESC 'K
OPANO: a group of Kopano' SUP top AUXILIARY MUST cn MAY ( kopanoAccount $ k
opanoHidden $ mail $ kopanoAliases $ kopanoSecurityGroup $ kopanoSendAsPriv
ilege $ gidNumber ) )
olcObjectClasses: {3}( 1.3.6.1.4.1.47732.1.3.0.0 NAME 'kopano-company' DESC
'KOPANO: a company of Kopano' SUP top AUXILIARY MUST ou MAY ( kopanoAccount
$ kopanoHidden $ kopanoViewPrivilege $ kopanoAdminPrivilege $ kopanoSystem
Admin $ kopanoQuotaOverride $ kopanoQuotaWarn $ kopanoUserDefaultQuotaOverr
ide $ kopanoUserDefaultQuotaWarn $ kopanoUserDefaultQuotaSoft $ kopanoUserD
efaultQuotaHard $ kopanoQuotaUserWarningRecipients $ kopanoQuotaCompanyWarn
ingRecipients $ kopanoCompanyServer ) )
olcObjectClasses: {4}( 1.3.6.1.4.1.47732.1.4.0.0 NAME 'kopano-server' DESC '
KOPANO: a Kopano server' SUP top AUXILIARY MUST cn MAY ( kopanoAccount $ ko
panoHidden $ kopanoHttpPort $ kopanoSslPort $ kopanoFilePath $ kopanoContai
nsPublic $ kopanoProxyURL ) )
olcObjectClasses: {5}( 1.3.6.1.4.1.47732.1.5.0.0 NAME 'kopano-addresslist' D
ESC 'KOPANO: a Kopano Addresslist' SUP top STRUCTURAL MUST cn MAY ( kopanoA
ccount $ kopanoHidden $ kopanoFilter $ kopanoBase ) )
olcObjectClasses: {6}( 1.3.6.1.4.1.47732.1.7.0.0 NAME 'kopano-dynamicgroup'
DESC 'KOPANO: a Kopano dynamic group' SUP top STRUCTURAL MUST cn MAY ( kopa
noAccount $ kopanoHidden $ mail $ kopanoAliases $ kopanoFilter $ kopanoBase
) )
structuralObjectClass: olcSchemaConfig
entryUUID: db7af548-4dc9-1040-9b57-b713c26c3953
creatorsName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
createTimestamp: 20251104125935Z
entryCSN: 20251104125935.051568Z#000000#000#000000
modifiersName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
modifyTimestamp: 20251104125935Z

View File

@ -1,18 +0,0 @@
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 3534bf13
dn: cn={6}openssh-lpk
objectClass: olcSchemaConfig
cn: {6}openssh-lpk
olcAttributeTypes: {0}( 1.3.6.1.4.1.24552.500.1.1.1.13 NAME 'sshPublicKey' D
ESC 'MANDATORY: OpenSSH Public key' EQUALITY octetStringMatch SYNTAX 1.3.6.
1.4.1.1466.115.121.1.40 )
olcObjectClasses: {0}( 1.3.6.1.4.1.24552.500.1.1.2.0 NAME 'ldapPublicKey' DE
SC 'MANDATORY: OpenSSH LPK objectclass' SUP top AUXILIARY MAY ( sshPublicKe
y $ uid ) )
structuralObjectClass: olcSchemaConfig
entryUUID: db817512-4dc9-1040-9b58-b713c26c3953
creatorsName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
createTimestamp: 20251104125935Z
entryCSN: 20251104125935.094162Z#000000#000#000000
modifiersName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
modifyTimestamp: 20251104125935Z

View File

@ -1,50 +0,0 @@
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 74ba36de
dn: cn={7}postfix-book
objectClass: olcSchemaConfig
cn: {7}postfix-book
olcAttributeTypes: {0}( 1.3.6.1.4.1.29426.1.10.1 NAME 'mailHomeDirectory' DE
SC 'The absolute path to the mail user home directory' EQUALITY caseExactIA
5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {1}( 1.3.6.1.4.1.29426.1.10.2 NAME 'mailAlias' DESC 'RFC8
22 Mailbox - mail alias' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5Su
bstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} )
olcAttributeTypes: {2}( 1.3.6.1.4.1.29426.1.10.3 NAME 'mailUidNumber' DESC '
UID required to access the mailbox' EQUALITY integerMatch SYNTAX 1.3.6.1.4.
1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {3}( 1.3.6.1.4.1.29426.1.10.4 NAME 'mailGidNumber' DESC '
GID required to access the mailbox' EQUALITY integerMatch SYNTAX 1.3.6.1.4.
1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {4}( 1.3.6.1.4.1.29426.1.10.5 NAME 'mailEnabled' DESC 'TR
UE to enable, FALSE to disable account' EQUALITY booleanMatch SYNTAX 1.3.6.
1.4.1.1466.115.121.1.7 SINGLE-VALUE )
olcAttributeTypes: {5}( 1.3.6.1.4.1.29426.1.10.6 NAME 'mailGroupMember' DESC
'Name of a mail distribution list' EQUALITY caseExactIA5Match SYNTAX 1.3.6
.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {6}( 1.3.6.1.4.1.29426.1.10.7 NAME 'mailQuota' DESC 'Mail
quota limit in kilobytes' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.14
66.115.121.1.26 )
olcAttributeTypes: {7}( 1.3.6.1.4.1.29426.1.10.8 NAME 'mailStorageDirectory'
DESC 'The absolute path to the mail users mailbox' EQUALITY caseExactIA5Ma
tch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {8}( 1.3.6.1.4.1.29426.1.10.9 NAME 'mailSieveRuleSource'
DESC 'Sun ONE Messaging Server defined attribute' SYNTAX 1.3.6.1.4.1.1466.1
15.121.1.26 X-ORIGIN 'Sun ONE Messaging Server' )
olcAttributeTypes: {9}( 1.3.6.1.4.1.29426.1.10.10 NAME 'mailForwardingAddres
s' DESC 'Address(es) to forward all incoming messages to.' EQUALITY caseIgn
oreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{320} )
olcObjectClasses: {0}( 1.3.6.1.4.1.29426.1.2.2.1 NAME 'PostfixBookMailAccoun
t' DESC 'Mail account used in Postfix Book' SUP top AUXILIARY MUST mail MAY
( mailHomeDirectory $ mailAlias $ mailGroupMember $ mailUidNumber $ mailGi
dNumber $ mailEnabled $ mailQuota $ mailStorageDirectory $ mailSieveRuleSou
rce ) )
olcObjectClasses: {1}( 1.3.6.1.4.1.29426.1.2.2.2 NAME 'PostfixBookMailForwar
d' DESC 'Mail forward used in Postfix Book' SUP top AUXILIARY MUST ( mail $
mailAlias ) MAY mailForwardingAddress )
structuralObjectClass: olcSchemaConfig
entryUUID: db873fd8-4dc9-1040-9b59-b713c26c3953
creatorsName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
createTimestamp: 20251104125935Z
entryCSN: 20251104125935.132121Z#000000#000#000000
modifiersName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
modifyTimestamp: 20251104125935Z

View File

@ -1,235 +0,0 @@
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 1b60dfa9
dn: cn={8}samba
objectClass: olcSchemaConfig
cn: {8}samba
olcAttributeTypes: {0}( 1.3.6.1.4.1.7165.2.1.24 NAME 'sambaLMPassword' DESC
'LanManager Password' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.1
15.121.1.26{32} SINGLE-VALUE )
olcAttributeTypes: {1}( 1.3.6.1.4.1.7165.2.1.25 NAME 'sambaNTPassword' DESC
'MD4 hash of the unicode password' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6
.1.4.1.1466.115.121.1.26{32} SINGLE-VALUE )
olcAttributeTypes: {2}( 1.3.6.1.4.1.7165.2.1.26 NAME 'sambaAcctFlags' DESC '
Account Flags' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.
1.26{16} SINGLE-VALUE )
olcAttributeTypes: {3}( 1.3.6.1.4.1.7165.2.1.27 NAME 'sambaPwdLastSet' DESC
'Timestamp of the last password update' EQUALITY integerMatch SYNTAX 1.3.6.
1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {4}( 1.3.6.1.4.1.7165.2.1.28 NAME 'sambaPwdCanChange' DES
C 'Timestamp of when the user is allowed to update the password' EQUALITY i
ntegerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {5}( 1.3.6.1.4.1.7165.2.1.29 NAME 'sambaPwdMustChange' DE
SC 'Timestamp of when the password will expire' EQUALITY integerMatch SYNTA
X 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {6}( 1.3.6.1.4.1.7165.2.1.30 NAME 'sambaLogonTime' DESC '
Timestamp of last logon' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.
121.1.27 SINGLE-VALUE )
olcAttributeTypes: {7}( 1.3.6.1.4.1.7165.2.1.31 NAME 'sambaLogoffTime' DESC
'Timestamp of last logoff' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.11
5.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {8}( 1.3.6.1.4.1.7165.2.1.32 NAME 'sambaKickoffTime' DESC
'Timestamp of when the user will be logged off automatically' EQUALITY int
egerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {9}( 1.3.6.1.4.1.7165.2.1.48 NAME 'sambaBadPasswordCount'
DESC 'Bad password attempt count' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1
.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {10}( 1.3.6.1.4.1.7165.2.1.49 NAME 'sambaBadPasswordTime'
DESC 'Time of the last bad password attempt' EQUALITY integerMatch SYNTAX
1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {11}( 1.3.6.1.4.1.7165.2.1.55 NAME 'sambaLogonHours' DESC
'Logon Hours' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.
1.26{42} SINGLE-VALUE )
olcAttributeTypes: {12}( 1.3.6.1.4.1.7165.2.1.33 NAME 'sambaHomeDrive' DESC
'Driver letter of home directory mapping' EQUALITY caseIgnoreIA5Match SYNTA
X 1.3.6.1.4.1.1466.115.121.1.26{4} SINGLE-VALUE )
olcAttributeTypes: {13}( 1.3.6.1.4.1.7165.2.1.34 NAME 'sambaLogonScript' DES
C 'Logon script path' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.
121.1.15{255} SINGLE-VALUE )
olcAttributeTypes: {14}( 1.3.6.1.4.1.7165.2.1.35 NAME 'sambaProfilePath' DES
C 'Roaming profile path' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.1
15.121.1.15{255} SINGLE-VALUE )
olcAttributeTypes: {15}( 1.3.6.1.4.1.7165.2.1.36 NAME 'sambaUserWorkstations
' DESC 'List of user workstations the user is allowed to logon to' EQUALITY
caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{255} SINGLE-VALUE )
olcAttributeTypes: {16}( 1.3.6.1.4.1.7165.2.1.37 NAME 'sambaHomePath' DESC '
Home directory UNC path' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.1
15.121.1.15{128} )
olcAttributeTypes: {17}( 1.3.6.1.4.1.7165.2.1.38 NAME 'sambaDomainName' DESC
'Windows NT domain to which the user belongs' EQUALITY caseIgnoreMatch SYN
TAX 1.3.6.1.4.1.1466.115.121.1.15{128} )
olcAttributeTypes: {18}( 1.3.6.1.4.1.7165.2.1.47 NAME 'sambaMungedDial' DESC
'Base64 encoded user parameter string' EQUALITY caseExactMatch SYNTAX 1.3.
6.1.4.1.1466.115.121.1.15{1050} )
olcAttributeTypes: {19}( 1.3.6.1.4.1.7165.2.1.54 NAME 'sambaPasswordHistory'
DESC 'Concatenated MD5 hashes of the salted NT passwords used on this acco
unt' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{32} )
olcAttributeTypes: {20}( 1.3.6.1.4.1.7165.2.1.20 NAME 'sambaSID' DESC 'Secur
ity ID' EQUALITY caseIgnoreIA5Match SUBSTR caseExactIA5SubstringsMatch SYNT
AX 1.3.6.1.4.1.1466.115.121.1.26{64} SINGLE-VALUE )
olcAttributeTypes: {21}( 1.3.6.1.4.1.7165.2.1.23 NAME 'sambaPrimaryGroupSID'
DESC 'Primary Group Security ID' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.
1.4.1.1466.115.121.1.26{64} SINGLE-VALUE )
olcAttributeTypes: {22}( 1.3.6.1.4.1.7165.2.1.51 NAME 'sambaSIDList' DESC 'S
ecurity ID List' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.12
1.1.26{64} )
olcAttributeTypes: {23}( 1.3.6.1.4.1.7165.2.1.19 NAME 'sambaGroupType' DESC
'NT Group Type' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
olcAttributeTypes: {24}( 1.3.6.1.4.1.7165.2.1.21 NAME 'sambaNextUserRid' DES
C 'Next NT rid to give our for users' EQUALITY integerMatch SYNTAX 1.3.6.1.
4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {25}( 1.3.6.1.4.1.7165.2.1.22 NAME 'sambaNextGroupRid' DE
SC 'Next NT rid to give out for groups' EQUALITY integerMatch SYNTAX 1.3.6.
1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {26}( 1.3.6.1.4.1.7165.2.1.39 NAME 'sambaNextRid' DESC 'N
ext NT rid to give out for anything' EQUALITY integerMatch SYNTAX 1.3.6.1.4
.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {27}( 1.3.6.1.4.1.7165.2.1.40 NAME 'sambaAlgorithmicRidBa
se' DESC 'Base at which the samba RID generation algorithm should operate'
EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {28}( 1.3.6.1.4.1.7165.2.1.41 NAME 'sambaShareName' DESC
'Share Name' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
SINGLE-VALUE )
olcAttributeTypes: {29}( 1.3.6.1.4.1.7165.2.1.42 NAME 'sambaOptionName' DESC
'Option Name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SY
NTAX 1.3.6.1.4.1.1466.115.121.1.15{256} )
olcAttributeTypes: {30}( 1.3.6.1.4.1.7165.2.1.43 NAME 'sambaBoolOption' DESC
'A boolean option' EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1
.7 SINGLE-VALUE )
olcAttributeTypes: {31}( 1.3.6.1.4.1.7165.2.1.44 NAME 'sambaIntegerOption' D
ESC 'An integer option' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.1
21.1.27 SINGLE-VALUE )
olcAttributeTypes: {32}( 1.3.6.1.4.1.7165.2.1.45 NAME 'sambaStringOption' DE
SC 'A string option' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115
.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {33}( 1.3.6.1.4.1.7165.2.1.46 NAME 'sambaStringListOption
' DESC 'A string list option' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1
466.115.121.1.15 )
olcAttributeTypes: {34}( 1.3.6.1.4.1.7165.2.1.53 NAME 'sambaTrustFlags' DESC
'Trust Password Flags' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466
.115.121.1.26 )
olcAttributeTypes: {35}( 1.3.6.1.4.1.7165.2.1.58 NAME 'sambaMinPwdLength' DE
SC 'Minimal password length (default: 5)' EQUALITY integerMatch SYNTAX 1.3.
6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {36}( 1.3.6.1.4.1.7165.2.1.59 NAME 'sambaPwdHistoryLength
' DESC 'Length of Password History Entries (default: 0 => off)' EQUALITY in
tegerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {37}( 1.3.6.1.4.1.7165.2.1.60 NAME 'sambaLogonToChgPwd' D
ESC 'Force Users to logon for password change (default: 0 => off, 2 => on)'
EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {38}( 1.3.6.1.4.1.7165.2.1.61 NAME 'sambaMaxPwdAge' DESC
'Maximum password age, in seconds (default: -1 => never expire passwords)'
EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {39}( 1.3.6.1.4.1.7165.2.1.62 NAME 'sambaMinPwdAge' DESC
'Minimum password age, in seconds (default: 0 => allow immediate password c
hange)' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-V
ALUE )
olcAttributeTypes: {40}( 1.3.6.1.4.1.7165.2.1.63 NAME 'sambaLockoutDuration'
DESC 'Lockout duration in minutes (default: 30, -1 => forever)' EQUALITY i
ntegerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {41}( 1.3.6.1.4.1.7165.2.1.64 NAME 'sambaLockoutObservati
onWindow' DESC 'Reset time after lockout in minutes (default: 30)' EQUALITY
integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {42}( 1.3.6.1.4.1.7165.2.1.65 NAME 'sambaLockoutThreshold
' DESC 'Lockout users after bad logon attempts (default: 0 => off)' EQUALIT
Y integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {43}( 1.3.6.1.4.1.7165.2.1.66 NAME 'sambaForceLogoff' DES
C 'Disconnect Users outside logon hours (default: -1 => off, 0 => on)' EQUA
LITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {44}( 1.3.6.1.4.1.7165.2.1.67 NAME 'sambaRefuseMachinePwd
Change' DESC 'Allow Machine Password changes (default: 0 => off)' EQUALITY
integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {45}( 1.3.6.1.4.1.7165.2.1.68 NAME 'sambaClearTextPasswor
d' DESC 'Clear text password (used for trusted domain passwords)' EQUALITY
octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
olcAttributeTypes: {46}( 1.3.6.1.4.1.7165.2.1.69 NAME 'sambaPreviousClearTex
tPassword' DESC 'Previous clear text password (used for trusted domain pass
words)' EQUALITY octetStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )
olcAttributeTypes: {47}( 1.3.6.1.4.1.7165.2.1.70 NAME 'sambaTrustType' DESC
'Type of trust' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
SINGLE-VALUE )
olcAttributeTypes: {48}( 1.3.6.1.4.1.7165.2.1.71 NAME 'sambaTrustAttributes'
DESC 'Trust attributes for a trusted domain' EQUALITY integerMatch SYNTAX
1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {49}( 1.3.6.1.4.1.7165.2.1.72 NAME 'sambaTrustDirection'
DESC 'Direction of a trust' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.1
15.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {50}( 1.3.6.1.4.1.7165.2.1.73 NAME 'sambaTrustPartner' DE
SC 'Fully qualified name of the domain with which a trust exists' EQUALITY
caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} )
olcAttributeTypes: {51}( 1.3.6.1.4.1.7165.2.1.74 NAME 'sambaFlatName' DESC '
NetBIOS name of a domain' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.
115.121.1.15{128} )
olcAttributeTypes: {52}( 1.3.6.1.4.1.7165.2.1.75 NAME 'sambaTrustAuthOutgoin
g' DESC 'Authentication information for the outgoing portion of a trust' EQ
UALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1050} )
olcAttributeTypes: {53}( 1.3.6.1.4.1.7165.2.1.76 NAME 'sambaTrustAuthIncomin
g' DESC 'Authentication information for the incoming portion of a trust' EQ
UALITY caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1050} )
olcAttributeTypes: {54}( 1.3.6.1.4.1.7165.2.1.77 NAME 'sambaSecurityIdentifi
er' DESC 'SID of a trusted domain' EQUALITY caseIgnoreIA5Match SUBSTR caseE
xactIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{64} SINGLE-VALU
E )
olcAttributeTypes: {55}( 1.3.6.1.4.1.7165.2.1.78 NAME 'sambaTrustForestTrust
Info' DESC 'Forest trust information for a trusted domain object' EQUALITY
caseExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{1050} )
olcAttributeTypes: {56}( 1.3.6.1.4.1.7165.2.1.79 NAME 'sambaTrustPosixOffset
' DESC 'POSIX offset of a trust' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1
466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {57}( 1.3.6.1.4.1.7165.2.1.80 NAME 'sambaSupportedEncrypt
ionTypes' DESC 'Supported encryption types of a trust' EQUALITY integerMatc
h SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcObjectClasses: {0}( 1.3.6.1.4.1.7165.2.2.6 NAME 'sambaSamAccount' DESC 'S
amba 3.0 Auxilary SAM Account' SUP top AUXILIARY MUST ( uid $ sambaSID ) MA
Y ( cn $ sambaLMPassword $ sambaNTPassword $ sambaPwdLastSet $ sambaLogonTi
me $ sambaLogoffTime $ sambaKickoffTime $ sambaPwdCanChange $ sambaPwdMustC
hange $ sambaAcctFlags $ displayName $ sambaHomePath $ sambaHomeDrive $ sam
baLogonScript $ sambaProfilePath $ description $ sambaUserWorkstations $ sa
mbaPrimaryGroupSID $ sambaDomainName $ sambaMungedDial $ sambaBadPasswordCo
unt $ sambaBadPasswordTime $ sambaPasswordHistory $ sambaLogonHours ) )
olcObjectClasses: {1}( 1.3.6.1.4.1.7165.2.2.4 NAME 'sambaGroupMapping' DESC
'Samba Group Mapping' SUP top AUXILIARY MUST ( gidNumber $ sambaSID $ samba
GroupType ) MAY ( displayName $ description $ sambaSIDList ) )
olcObjectClasses: {2}( 1.3.6.1.4.1.7165.2.2.14 NAME 'sambaTrustPassword' DES
C 'Samba Trust Password' SUP top STRUCTURAL MUST ( sambaDomainName $ sambaN
TPassword $ sambaTrustFlags ) MAY ( sambaSID $ sambaPwdLastSet ) )
olcObjectClasses: {3}( 1.3.6.1.4.1.7165.2.2.15 NAME 'sambaTrustedDomainPassw
ord' DESC 'Samba Trusted Domain Password' SUP top STRUCTURAL MUST ( sambaDo
mainName $ sambaSID $ sambaClearTextPassword $ sambaPwdLastSet ) MAY sambaP
reviousClearTextPassword )
olcObjectClasses: {4}( 1.3.6.1.4.1.7165.2.2.5 NAME 'sambaDomain' DESC 'Samba
Domain Information' SUP top STRUCTURAL MUST ( sambaDomainName $ sambaSID )
MAY ( sambaNextRid $ sambaNextGroupRid $ sambaNextUserRid $ sambaAlgorithm
icRidBase $ sambaMinPwdLength $ sambaPwdHistoryLength $ sambaLogonToChgPwd
$ sambaMaxPwdAge $ sambaMinPwdAge $ sambaLockoutDuration $ sambaLockoutObse
rvationWindow $ sambaLockoutThreshold $ sambaForceLogoff $ sambaRefuseMachi
nePwdChange ) )
olcObjectClasses: {5}( 1.3.6.1.4.1.7165.2.2.7 NAME 'sambaUnixIdPool' DESC 'P
ool for allocating UNIX uids/gids' SUP top AUXILIARY MUST ( uidNumber $ gid
Number ) )
olcObjectClasses: {6}( 1.3.6.1.4.1.7165.2.2.8 NAME 'sambaIdmapEntry' DESC 'M
apping from a SID to an ID' SUP top AUXILIARY MUST sambaSID MAY ( uidNumber
$ gidNumber ) )
olcObjectClasses: {7}( 1.3.6.1.4.1.7165.2.2.9 NAME 'sambaSidEntry' DESC 'Str
uctural Class for a SID' SUP top STRUCTURAL MUST sambaSID )
olcObjectClasses: {8}( 1.3.6.1.4.1.7165.2.2.10 NAME 'sambaConfig' DESC 'Samb
a Configuration Section' SUP top AUXILIARY MAY description )
olcObjectClasses: {9}( 1.3.6.1.4.1.7165.2.2.11 NAME 'sambaShare' DESC 'Samba
Share Section' SUP top STRUCTURAL MUST sambaShareName MAY description )
olcObjectClasses: {10}( 1.3.6.1.4.1.7165.2.2.12 NAME 'sambaConfigOption' DES
C 'Samba Configuration Option' SUP top STRUCTURAL MUST sambaOptionName MAY
( sambaBoolOption $ sambaIntegerOption $ sambaStringOption $ sambaStringLis
toption $ description ) )
olcObjectClasses: {11}( 1.3.6.1.4.1.7165.2.2.16 NAME 'sambaTrustedDomain' DE
SC 'Samba Trusted Domain Object' SUP top STRUCTURAL MUST cn MAY ( sambaTrus
tType $ sambaTrustAttributes $ sambaTrustDirection $ sambaTrustPartner $ sa
mbaFlatName $ sambaTrustAuthOutgoing $ sambaTrustAuthIncoming $ sambaSecuri
tyIdentifier $ sambaTrustForestTrustInfo $ sambaTrustPosixOffset $ sambaSup
portedEncryptionTypes ) )
structuralObjectClass: olcSchemaConfig
entryUUID: db8dd24e-4dc9-1040-9b5a-b713c26c3953
creatorsName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
createTimestamp: 20251104125935Z
entryCSN: 20251104125935.175192Z#000000#000#000000
modifiersName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
modifyTimestamp: 20251104125935Z

View File

@ -1,18 +0,0 @@
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 cf95d409
dn: olcDatabase={-1}frontend
objectClass: olcDatabaseConfig
objectClass: olcFrontendConfig
olcDatabase: {-1}frontend
olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=extern
al,cn=auth manage by * break
olcAccess: {1}to dn.exact="" by * read
olcAccess: {2}to dn.base="cn=Subschema" by * read
olcSizeLimit: 500
structuralObjectClass: olcDatabaseConfig
entryUUID: db0bbf2a-4dc9-1040-847d-a5a6e7d3c4b9
creatorsName: cn=config
createTimestamp: 20251104125934Z
entryCSN: 20251104125934.322734Z#000000#000#000000
modifiersName: cn=config
modifyTimestamp: 20251104125934Z

View File

@ -1,16 +0,0 @@
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 a219ffe3
dn: olcDatabase={0}config
objectClass: olcDatabaseConfig
olcDatabase: {0}config
olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=extern
al,cn=auth manage by * break
olcRootDN: cn=admin,cn=config
structuralObjectClass: olcDatabaseConfig
entryUUID: db0e32fa-4dc9-1040-847e-a5a6e7d3c4b9
creatorsName: cn=config
createTimestamp: 20251104125934Z
olcRootPW:: e1NTSEF9b1kwMHBTQlFuVDdzbWxXN2JsYTBXQ0xtWDlFeXY0NGI=
entryCSN: 20251104125935.244807Z#000000#000#000000
modifiersName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
modifyTimestamp: 20251104125935Z

View File

@ -1,32 +0,0 @@
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 071b7a13
dn: olcDatabase={1}mdb
objectClass: olcDatabaseConfig
objectClass: olcMdbConfig
olcDatabase: {1}mdb
olcDbDirectory: /var/lib/ldap
olcSuffix: dc=test,dc=local
olcLastMod: TRUE
olcRootDN: cn=admin,dc=test,dc=local
olcRootPW:: e1NTSEF9aU9iSzRmb1MrWXJSdWxna2pXVndHUXdocVFsR1pvTW8=
olcDbCheckpoint: 512 30
olcDbMaxSize: 1073741824
structuralObjectClass: olcMdbConfig
entryUUID: db1780c6-4dc9-1040-8485-a5a6e7d3c4b9
creatorsName: cn=admin,cn=config
createTimestamp: 20251104125934Z
olcAccess: {0}to * by dn.exact=gidNumber=0+uidNumber=0,cn=peercred,cn=extern
al,cn=auth manage by * break
olcAccess: {1}to attrs=userPassword,shadowLastChange by self write by dn="cn
=admin,dc=test,dc=local" write by anonymous auth by * none
olcAccess: {2}to * by self read by dn="cn=admin,dc=test,dc=local" write by *
none
olcDbIndex: uid eq
olcDbIndex: mail eq
olcDbIndex: memberOf eq
olcDbIndex: entryCSN eq
olcDbIndex: entryUUID eq
olcDbIndex: objectClass eq
entryCSN: 20251104125935.424343Z#000000#000#000000
modifiersName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
modifyTimestamp: 20251104125935Z

View File

@ -1,18 +0,0 @@
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 854b8228
dn: olcOverlay={0}memberof
objectClass: olcOverlayConfig
objectClass: olcMemberOf
olcOverlay: {0}memberof
olcMemberOfDangling: ignore
olcMemberOfRefInt: TRUE
olcMemberOfGroupOC: groupOfUniqueNames
olcMemberOfMemberAD: uniqueMember
olcMemberOfMemberOfAD: memberOf
structuralObjectClass: olcMemberOf
entryUUID: dba68d48-4dc9-1040-9b5b-b713c26c3953
creatorsName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
createTimestamp: 20251104125935Z
entryCSN: 20251104125935.337265Z#000000#000#000000
modifiersName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
modifyTimestamp: 20251104125935Z

View File

@ -1,18 +0,0 @@
# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.
# CRC32 88c37d33
dn: olcOverlay={1}refint
objectClass: olcOverlayConfig
objectClass: olcRefintConfig
olcOverlay: {1}refint
olcRefintAttribute: owner
olcRefintAttribute: manager
olcRefintAttribute: uniqueMember
olcRefintAttribute: member
olcRefintAttribute: memberOf
structuralObjectClass: olcRefintConfig
entryUUID: dbadc004-4dc9-1040-9b5c-b713c26c3953
creatorsName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
createTimestamp: 20251104125935Z
entryCSN: 20251104125935.384438Z#000000#000#000000
modifiersName: gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth
modifyTimestamp: 20251104125935Z

View File

@ -1,229 +0,0 @@
# LDAP Integration Tests
## Описание
Интеграционные тесты для проверки работы с реальным LDAP сервером в Docker.
## Быстрый старт
### 1. Запуск LDAP сервера
```bash
cd backend
docker-compose -f docker-compose.test.yml up -d
```
**Сервисы:**
- OpenLDAP: `ldap://localhost:10389`
- phpLDAPadmin (веб-интерфейс): http://localhost:8090
**Учётные данные:**
- Admin DN: `cn=admin,dc=test,dc=local`
- Admin Password: `test_change-me-redacted-secret`
### 2. Проверка работы LDAP
```bash
# Через ldapsearch (если установлен)
ldapsearch -x -H ldap://localhost:10389 -b "dc=test,dc=local" \
-D "cn=admin,dc=test,dc=local" -w test_change-me-redacted-secret
# Через веб-интерфейс
# Откройте http://localhost:8090
# Login DN: cn=admin,dc=test,dc=local
# Password: test_change-me-redacted-secret
```
### 3. Установка зависимостей для тестов
```bash
pip install python-ldap
```
### 4. Запуск интеграционных тестов
```bash
# Все интеграционные тесты
pytest -m integration tests/integration/ -v
# Конкретный тест
pytest tests/integration/test_ldap_integration.py::test_ldap_server_is_accessible -v
# С остановкой на первой ошибке
pytest -m integration tests/integration/ -v -x
# С подробным выводом
pytest -m integration tests/integration/ -v -s
```
### 5. Остановка LDAP сервера
```bash
# Остановить с сохранением данных
docker-compose -f docker-compose.test.yml stop
# Остановить и удалить (очистить данные)
docker-compose -f docker-compose.test.yml down -v
```
## Структура
```
tests/
├── integration/
│ ├── conftest.py # Фикстуры для интеграционных тестов
│ └── test_ldap_integration.py # Интеграционные тесты LDAP
└── ldap_fixtures/
└── 01-base-structure.ldif # Начальная структура LDAP
```
## Конфигурация тестового LDAP
**База данных:**
- Base DN: `dc=test,dc=local`
- Users OU: `ou=Users,dc=test,dc=local`
- Groups OU: `ou=Groups,dc=test,dc=local`
- Departments OU: `ou=Departments,dc=test,dc=local`
**Тестовые данные:**
- Пользователь: `cn=test.user,ou=Users,dc=test,dc=local`
- Группа: `cn=test-group,ou=Groups,dc=test,dc=local`
- Отдел: `ou=IT,ou=Departments,dc=test,dc=local`
## Созданные тесты
### ✅ test_ldap_server_is_accessible
Проверяет доступность LDAP сервера и базовую структуру.
### 🔧 test_register_user_creates_in_ldap
Тестирует создание пользователя через API с записью в LDAP.
**Статус:** Требует настройки DirectoryService на тестовый LDAP
### 🔧 test_verify_email_activates_in_ldap
Тестирует верификацию email и активацию в LDAP.
**Статус:** Требует настройки DirectoryService на тестовый LDAP
### 🔧 test_create_group_syncs_to_ldap
Тестирует создание группы с синхронизацией в LDAP.
**Статус:** Требует настройки DirectoryService на тестовый LDAP
### 🔧 test_full_user_lifecycle_with_ldap
Тестирует полный жизненный цикл: регистрация → верификация → активация.
**Статус:** Требует настройки DirectoryService на тестовый LDAP
## TODO
### Следующие шаги:
1. **Настроить DirectoryService для тестов**
```python
# В conftest.py или settings_test.py
LDAP_SERVER_URI = "ldap://localhost:10389"
LDAP_BASE_DN = "dc=test,dc=local"
```
2. **Добавить хелперы для проверки LDAP**
```python
def assert_user_exists_in_ldap(username):
"""Проверяет наличие пользователя в LDAP"""
conn = ldap.initialize("ldap://localhost:10389")
# ...
```
3. **Расширить тестовое покрытие**
- Тесты отделов с LDAP
- Тесты позиций с LDAP
- Тесты синхронизации изменений
- Тесты удаления и деактивации
4. **Добавить CI/CD интеграцию**
```yaml
# .github/workflows/tests.yml
- name: Start LDAP server
run: docker-compose -f docker-compose.test.yml up -d
- name: Run integration tests
run: pytest -m integration
```
## Отладка
### Просмотр логов LDAP
```bash
docker logs eusrr-test-ldap -f
```
### Подключение к контейнеру
```bash
docker exec -it eusrr-test-ldap bash
```
### Проверка данных в LDAP
```bash
# Список всех пользователей
docker exec eusrr-test-ldap ldapsearch -x -b "ou=Users,dc=test,dc=local" -D "cn=admin,dc=test,dc=local" -w test_change-me-redacted-secret
# Список всех групп
docker exec eusrr-test-ldap ldapsearch -x -b "ou=Groups,dc=test,dc=local" -D "cn=admin,dc=test,dc=local" -w test_change-me-redacted-secret
```
### Сброс данных LDAP
```bash
# Удалить все данные и начать заново
docker-compose -f docker-compose.test.yml down -v
docker-compose -f docker-compose.test.yml up -d
```
## Полезные команды
```bash
# Статус контейнеров
docker-compose -f docker-compose.test.yml ps
# Рестарт LDAP
docker-compose -f docker-compose.test.yml restart test-ldap
# Просмотр использования ресурсов
docker stats eusrr-test-ldap
# Экспорт данных LDAP
docker exec eusrr-test-ldap slapcat > ldap_backup.ldif
```
## Troubleshooting
### Проблема: LDAP сервер не запускается
**Решение:**
```bash
# Проверить логи
docker logs eusrr-test-ldap
# Проверить занят ли порт
netstat -ano | findstr :10389
# Убить процесс если порт занят
taskkill /PID <PID> /F
```
### Проблема: Тесты не могут подключиться к LDAP
**Решение:**
1. Проверить что контейнер запущен: `docker ps`
2. Проверить доступность порта: `telnet localhost 10389`
3. Увеличить время ожидания в `conftest.py`
### Проблема: python-ldap не устанавливается на Windows
**Решение:**
```bash
# Установить готовый wheel
pip install https://download.lfd.uci.edu/pythonlibs/archived/python_ldap-3.4.0-cp313-cp313-win_amd64.whl
# Или использовать ldap3 (альтернатива)
pip install ldap3
```

View File

@ -1,221 +0,0 @@
# 🧪 Интеграционные тесты с LDAP
Простые интеграционные тесты с реальным LDAP сервером.
## 🎯 Подход
**Держите LDAP сервер запущенным постоянно во время разработки.**
Не нужны сложные фикстуры для управления Docker - просто запустите LDAP один раз и пишите обычные pytest тесты.
## 🚀 Быстрый старт
### 1. Запустите LDAP сервер (один раз)
```bash
cd backend
./ldap-test.sh start
```
LDAP сервер запустится в Docker и будет работать в фоне.
### 2. Запустите тесты
```bash
# Все интеграционные тесты
pytest tests/integration/ -v
# Конкретный тест
pytest tests/integration/test_ldap_simple.py::TestLDAPIntegration::test_register_user_creates_in_ldap -v
# С подробным выводом
pytest tests/integration/ -v -s
```
### 3. Пишите новые тесты
Используйте простые фикстуры из `conftest.py`:
```python
@pytest.mark.django_db
def test_my_feature(self, ldap_test_settings, ldap_connection):
"""Мой тест с LDAP"""
# Arrange
username = "test_myfeature"
# Act
# ... ваш код ...
# Assert - проверка в LDAP
from .conftest import ldap_search_user
ldap_user = ldap_search_user(ldap_connection, username)
assert ldap_user is not None
```
## 📋 Доступные helper функции
В `conftest.py` есть готовые функции для работы с LDAP:
- **`ldap_search_user(ldap_connection, username)`** - найти пользователя в LDAP
- **`ldap_delete_user(ldap_connection, username)`** - удалить пользователя из LDAP
- **`ldap_cleanup_test_users(ldap_connection, prefix="test_")`** - очистить всех тестовых пользователей
- **`get_ldap_connection(ldap_connection)`** - получить прямое подключение к LDAP
### Фикстуры
- **`ldap_connection`** (session) - параметры подключения к LDAP
- **`ldap_test_settings`** - временно переключает Django на тестовый LDAP
- **`cleanup_test_data`** (autouse) - автоматически очищает тестовые данные после каждого теста
## 🔧 Управление LDAP сервером
```bash
# Запустить
./ldap-test.sh start
# Остановить (но не удалять данные)
./ldap-test.sh stop
# Перезапустить
./ldap-test.sh restart
# Посмотреть логи
./ldap-test.sh logs
# Проверить что LDAP работает
./ldap-test.sh check
# Полная очистка (удалит все данные)
./ldap-test.sh clean
```
## 🌐 Веб-интерфейс phpLDAPadmin
Для визуального просмотра данных в LDAP:
```bash
start http://localhost:8090
```
**Вход:**
- Login DN: `cn=admin,dc=test,dc=local`
- Password: `test_change-me-redacted-secret`
Здесь можно посмотреть структуру LDAP, созданных пользователей, группы и т.д.
## 📝 Структура тестов
```
tests/integration/
├── conftest.py # Фикстуры и helper функции
├── test_ldap_simple.py # Простые интеграционные тесты
└── README.md # Эта инструкция
```
## 🎨 Пример теста
```python
import pytest
from django.test import TestCase
from .conftest import ldap_search_user
class TestMyFeature(TestCase):
@pytest.mark.django_db
def test_user_creation(self, ldap_test_settings, ldap_connection):
"""Создание пользователя синхронизируется с LDAP"""
# Arrange
user_data = {
"username": "test_newuser",
"email": "newuser@test.local",
"password": "Password123!",
# ... другие поля
}
# Act
response = self.client.post("/api/v1/employees/register/", user_data)
# Assert - проверяем Django
assert response.status_code == 201
# Assert - проверяем LDAP
ldap_user = ldap_search_user(ldap_connection, "test_newuser")
assert ldap_user is not None
dn, attrs = ldap_user
assert b"newuser@test.local" in attrs.get("mail", [])
```
## 🐛 Отладка
### Проверить что LDAP запущен
```bash
docker ps | grep ldap
# Должен показать: eusrr-test-ldap
```
### Посмотреть логи LDAP
```bash
./ldap-test.sh logs
```
### Проверить подключение к LDAP
```bash
./ldap-test.sh check
```
### Очистить тестовые данные вручную
```python
from tests.integration.conftest import ldap_cleanup_test_users
ldap_connection = {
"uri": "ldap://localhost:10389",
"base_dn": "dc=test,dc=local",
"admin_dn": "cn=admin,dc=test,dc=local",
"change-me-redacted-secret": "test_change-me-redacted-secret",
"users_ou": "ou=Users,dc=test,dc=local",
}
ldap_cleanup_test_users(ldap_connection, prefix="test_")
```
## ✅ Преимущества этого подхода
1. **Простота** - обычные pytest тесты, никаких сложных фикстур
2. **Скорость** - LDAP запускается один раз, не перезапускается между тестами
3. **Удобство** - веб-интерфейс для визуального контроля
4. **Изоляция** - автоматическая очистка тестовых данных после каждого теста
5. **Реалистичность** - тесты работают с настоящим LDAP
## 🔄 Workflow разработки
1. **Утром** - запускаете LDAP сервер:
```bash
./ldap-test.sh start
```
2. **В течение дня** - пишете и запускаете тесты:
```bash
pytest tests/integration/ -v
```
3. **Вечером** - останавливаете сервер (опционально):
```bash
./ldap-test.sh stop
```
4. **При необходимости** - чистите данные:
```bash
./ldap-test.sh clean && ./ldap-test.sh start
```
## 📚 Следующие шаги
- [ ] Дописать проверки для полей в LDAP (accountStatus, etc)
- [ ] Добавить тесты для групп и департаментов
- [ ] Добавить тесты для обновления пользователей
- [ ] Протестировать синхронизацию при изменении данных

View File

@ -1,177 +0,0 @@
"""
Pytest fixtures для интеграционных тестов с LDAP.
ВАЖНО: Перед запуском тестов запустите LDAP сервер:
cd backend
./ldap-test.sh start
Запуск тестов:
pytest tests/integration/ -v
"""
import pytest
from ldap3 import Server, Connection, ALL, SUBTREE
from django.conf import settings
@pytest.fixture(scope="session")
def ldap_connection():
"""
Возвращает параметры подключения к тестовому LDAP серверу.
LDAP сервер должен быть запущен заранее: ./ldap-test.sh start
"""
return {
"uri": "ldap://localhost:10389",
"base_dn": "dc=test,dc=local",
"admin_dn": "cn=admin,dc=test,dc=local",
"change-me-redacted-secret": "test_change-me-redacted-secret",
"users_ou": "ou=Users,dc=test,dc=local",
"groups_ou": "ou=Groups,dc=test,dc=local",
"departments_ou": "ou=Departments,dc=test,dc=local",
}
@pytest.fixture
def ldap_test_settings(ldap_connection):
"""
Временно переключает Django настройки на тестовый LDAP сервер.
"""
original_settings = {
"LDAP_ENABLED": settings.LDAP_ENABLED,
"LDAP_URI": getattr(settings, "LDAP_URI", ""),
"LDAP_HOST": getattr(settings, "LDAP_HOST", ""),
"LDAP_PORT": getattr(settings, "LDAP_PORT", 389),
"LDAP_BASE_DN": getattr(settings, "LDAP_BASE_DN", ""),
"LDAP_BIND_DN": getattr(settings, "LDAP_BIND_DN", ""),
"LDAP_BIND_PASSWORD": getattr(settings, "LDAP_BIND_PASSWORD", ""),
"LDAP_GROUPS_BASE": getattr(settings, "LDAP_GROUPS_BASE", ""),
"LDAP_POSITIONS_BASE": getattr(settings, "LDAP_POSITIONS_BASE", ""),
"LDAP_UPN_SUFFIX": getattr(settings, "LDAP_UPN_SUFFIX", ""),
}
# Устанавливаем тестовые настройки
settings.LDAP_ENABLED = True
settings.LDAP_URI = ldap_connection["uri"]
settings.LDAP_HOST = "localhost"
settings.LDAP_PORT = 10389
settings.LDAP_BASE_DN = ldap_connection["base_dn"]
settings.LDAP_BIND_DN = ldap_connection["admin_dn"]
settings.LDAP_BIND_PASSWORD = ldap_connection["change-me-redacted-secret"]
settings.LDAP_USERS_BASE = ldap_connection["users_ou"]
settings.LDAP_GROUPS_BASE = ldap_connection["groups_ou"]
settings.LDAP_DEPARTMENTS_BASE = ldap_connection["departments_ou"]
settings.LDAP_POSITIONS_BASE = "ou=Positions,dc=test,dc=local"
settings.LDAP_UPN_SUFFIX = "@test.local"
yield ldap_connection
# Восстанавливаем оригинальные настройки
for key, value in original_settings.items():
setattr(settings, key, value)
# ============================================================================
# Helper функции для работы с LDAP
# ============================================================================
def get_ldap_connection(ldap_connection):
"""
Создает подключение к LDAP серверу.
Returns:
ldap3.Connection: Готовое подключение
"""
server = Server(ldap_connection["uri"], get_info=ALL)
conn = Connection(
server,
user=ldap_connection["admin_dn"],
password=ldap_connection["change-me-redacted-secret"],
auto_bind=True
)
return conn
def ldap_search_user(ldap_connection, username):
"""
Ищет пользователя в LDAP по username.
Args:
ldap_connection: Параметры подключения из фикстуры
username: Username для поиска
Returns:
dict или None: Найденный пользователь или None
"""
conn = get_ldap_connection(ldap_connection)
try:
conn.search(
ldap_connection["users_ou"],
f"(uid={username})",
search_scope=SUBTREE
)
if conn.entries:
entry = conn.entries[0]
return (entry.entry_dn, entry.entry_attributes_as_dict)
return None
finally:
conn.unbind()
def ldap_delete_user(ldap_connection, username):
"""
Удаляет пользователя из LDAP.
Args:
ldap_connection: Параметры подключения из фикстуры
username: Username для удаления
"""
user = ldap_search_user(ldap_connection, username)
if user:
dn, attrs = user
conn = get_ldap_connection(ldap_connection)
try:
conn.delete(dn)
finally:
conn.unbind()
def ldap_cleanup_test_users(ldap_connection, prefix="test_"):
"""
Удаляет всех тестовых пользователей (по префиксу).
Args:
ldap_connection: Параметры подключения из фикстуры
prefix: Префикс username для удаления (по умолчанию "test_")
"""
conn = get_ldap_connection(ldap_connection)
try:
conn.search(
ldap_connection["users_ou"],
f"(uid={prefix}*)",
search_scope=SUBTREE
)
for entry in conn.entries:
try:
conn.delete(entry.entry_dn)
except Exception:
pass # Игнорируем ошибки при удалении
finally:
conn.unbind()
@pytest.fixture
def cleanup_test_data(ldap_connection):
"""
Очищает тестовые данные после каждого теста.
Удаляет всех пользователей с префиксом "test_" из LDAP.
Используйте явно в тестах где нужна очистка.
"""
yield
# После теста удаляем тестовых пользователей
try:
ldap_cleanup_test_users(ldap_connection, prefix="test_")
except Exception as e:
print(f"⚠️ Не удалось очистить тестовые данные: {e}")

View File

@ -1,182 +0,0 @@
"""
Интеграционные тесты для проверки что код работает с LDAP включенным.
ВАЖНО: Перед запуском запустите LDAP сервер:
cd backend
./ldap-test.sh start
Эти тесты проверяют что:
1. Код не падает когда LDAP включен
2. Ошибки LDAP обрабатываются корректно
3. Fallback на БД работает когда LDAP недоступен
Запуск тестов:
pytest tests/integration/test_ldap_enabled.py -v
"""
import pytest
from rest_framework.test import APIClient
from employees.models import Employee
@pytest.mark.django_db
def test_register_with_ldap_enabled_handles_errors(ldap_test_settings):
"""
Регистрация с включенным LDAP корректно обрабатывает ошибки.
OpenLDAP не поддерживает AD атрибуты, поэтому ожидаем 502,
но важно что код не падает с 500.
"""
client = APIClient()
email = "test_error@test.local"
user_data = {
"email": email,
"password": "TestPassword123!",
"password_confirmation": "TestPassword123!",
"first_name": "Test",
"last_name": "Error",
"telegram": "@test_error",
"birth_date": "1990-01-01",
"phone_number": "+79001234567",
}
response = client.post("/api/v1/auth/register/", user_data)
# Ожидаем 502 (LDAP ошибка) или 201 (успех)
# но НЕ 500 (необработанное исключение)
assert response.status_code in [201, 502], \
f"Expected 201 or 502, got {response.status_code}: {response.data}"
if response.status_code == 502:
# Проверяем что ошибка понятная
assert "ldap" in str(response.data).lower()
print(f"✅ LDAP error handled correctly: {response.data}")
else:
# Если регистрация прошла - проверяем БД
assert Employee.objects.filter(email=email).exists()
print("✅ User registered successfully")
@pytest.mark.django_db
def test_register_without_ldap_works(ldap_connection):
"""
Регистрация работает когда LDAP отключен.
Это базовый тест что Django часть работает независимо от LDAP.
"""
from django.conf import settings
# Временно отключаем LDAP
original_enabled = settings.LDAP_ENABLED
settings.LDAP_ENABLED = False
try:
client = APIClient()
email = "test_noldap@test.local"
user_data = {
"email": email,
"password": "TestPassword123!",
"password_confirmation": "TestPassword123!",
"first_name": "Test",
"last_name": "NoLDAP",
"telegram": "@test_noldap",
"birth_date": "1990-01-01",
"phone_number": "+79001234568",
}
response = client.post("/api/v1/auth/register/", user_data)
# Без LDAP должно работать
assert response.status_code == 201, \
f"Expected 201, got {response.status_code}: {response.data}"
# Проверяем что пользователь создан в БД
employee = Employee.objects.get(email=email)
assert employee.email == user_data["email"]
assert employee.first_name == user_data["first_name"]
print(f"✅ User created without LDAP: {employee.email}")
finally:
# Восстанавливаем настройку
settings.LDAP_ENABLED = original_enabled
@pytest.mark.django_db
def test_verify_email_works_without_ldap(ldap_connection):
"""
Верификация email работает когда LDAP отключен.
"""
from django.conf import settings
# Временно отключаем LDAP
original_enabled = settings.LDAP_ENABLED
settings.LDAP_ENABLED = False
try:
client = APIClient()
email = "test_verify@test.local"
user_data = {
"email": email,
"password": "TestPassword123!",
"password_confirmation": "TestPassword123!",
"first_name": "Test",
"last_name": "Verify",
"telegram": "@test_verify",
"birth_date": "1990-01-01",
"phone_number": "+79001234569",
}
# Регистрируемся
response = client.post("/api/v1/auth/register/", user_data)
assert response.status_code == 201
# Получаем код активации
employee = Employee.objects.get(email=email)
code = employee.email_activation_code
assert code is not None
# Верифицируем email
verify_response = client.post(
"/api/v1/auth/verify-email/",
{"email": email, "code": code}
)
assert verify_response.status_code == 200
employee.refresh_from_db()
assert employee.email_verified is True
assert employee.is_active is True
print(f"✅ Email verified: {employee.email}")
finally:
settings.LDAP_ENABLED = original_enabled
@pytest.mark.django_db
def test_ldap_settings_are_properly_configured(
ldap_test_settings, ldap_connection
):
"""
Проверяет что тестовые LDAP параметры установлены правильно.
Это базовая проверка что фикстура ldap_test_settings работает.
"""
from django.conf import settings
assert settings.LDAP_ENABLED is True
assert settings.LDAP_URI == ldap_connection["uri"]
assert settings.LDAP_BASE_DN == ldap_connection["base_dn"]
assert settings.LDAP_BIND_DN == ldap_connection["admin_dn"]
assert settings.LDAP_BIND_PASSWORD == ldap_connection["change-me-redacted-secret"]
assert settings.LDAP_USERS_BASE == ldap_connection["users_ou"]
print("✅ LDAP settings configured correctly:")
print(f" URI: {settings.LDAP_URI}")
print(f" Base DN: {settings.LDAP_BASE_DN}")
print(f" Users Base: {settings.LDAP_USERS_BASE}")

View File

@ -1,213 +0,0 @@
"""
Интеграционные тесты для LDAP функциональности.
Требования:
1. Запущенный LDAP сервер: docker-compose -f docker-compose.test.yml up -d
2. Установленная библиотека: pip install python-ldap
Запуск:
pytest -m integration tests/integration/test_ldap_integration.py -v
Запуск с остановкой на первой ошибке:
pytest -m integration tests/integration/test_ldap_integration.py -v -x
"""
import pytest
from django.contrib.auth import get_user_model
from django.urls import reverse
from rest_framework import status
from employees.models import Group, Department
Employee = get_user_model()
pytestmark = pytest.mark.integration
@pytest.mark.django_db
def test_ldap_server_is_accessible(ldap_server):
"""Проверяет что LDAP сервер доступен"""
import ldap
conn = ldap.initialize(
f"ldap://{ldap_server['host']}:{ldap_server['port']}"
)
conn.simple_bind_s(
ldap_server['admin_dn'],
ldap_server['change-me-redacted-secret']
)
# Проверяем базовую структуру
results = conn.search_s(
ldap_server['base_dn'],
ldap.SCOPE_BASE,
"(objectClass=*)"
)
assert len(results) > 0
conn.unbind_s()
@pytest.mark.django_db
def test_register_user_creates_in_ldap(
api_client, ldap_test_settings, clean_ldap_users
):
"""INT-1: Регистрация пользователя создаёт запись в LDAP"""
url = reverse("api:v1:register")
data = {
"email": "ldap.test@test.local",
"first_name": "LDAP",
"last_name": "Test",
"phone_number": "+79990005555",
"password": "SecurePass123!",
"whatsapp": "+79990005555",
"birth_date": "1990-01-01",
}
response = api_client.post(url, data, format="json")
assert response.status_code == status.HTTP_201_CREATED
# Проверяем что пользователь создан в БД
user = Employee.objects.get(email="ldap.test@test.local")
assert user is not None
assert user.is_active is False # Не активен до верификации
# TODO: Проверить что запись создана в LDAP
# Требует настройку DirectoryService с тестовым LDAP
@pytest.mark.django_db
def test_verify_email_activates_in_ldap(
api_client, ldap_test_settings, clean_ldap_users
):
"""INT-2: Верификация email активирует пользователя в LDAP"""
# Сначала регистрируем
url = reverse("api:v1:register")
data = {
"email": "verify.test@test.local",
"first_name": "Verify",
"last_name": "Test",
"phone_number": "+79990006666",
"password": "SecurePass123!",
"whatsapp": "+79990006666",
"birth_date": "1990-01-01",
}
reg_response = api_client.post(url, data, format="json")
assert reg_response.status_code == status.HTTP_201_CREATED
# Получаем код верификации из БД
user = Employee.objects.get(email="verify.test@test.local")
verification_code = user.email_activation_code
# Верифицируем email
verify_url = reverse("api:v1:verify-email")
verify_data = {
"email": "verify.test@test.local",
"code": verification_code,
}
verify_response = api_client.post(verify_url, verify_data, format="json")
assert verify_response.status_code == status.HTTP_200_OK
# Проверяем активацию в БД
user.refresh_from_db()
assert user.is_active is True
assert user.email_verified is True
# TODO: Проверить активацию в LDAP
@pytest.mark.django_db
def test_create_group_syncs_to_ldap(
authenticated_client, ldap_test_settings, clean_ldap_users
):
"""INT-3: Создание группы синхронизируется в LDAP"""
url = reverse("api:v1:groups-list")
data = {
"name": "LDAP Test Group",
"cn": "ldap-test-group",
}
response = authenticated_client.post(url, data, format="json")
# Может быть 201 (успех) или 502 (LDAP ошибка)
assert response.status_code in [
status.HTTP_201_CREATED,
status.HTTP_502_BAD_GATEWAY
]
if response.status_code == status.HTTP_201_CREATED:
# Проверяем что группа создана в БД
group = Group.objects.get(cn="ldap-test-group")
assert group is not None
# TODO: Проверить что группа создана в LDAP
@pytest.mark.django_db
def test_full_user_lifecycle_with_ldap(
api_client, ldap_test_settings, clean_ldap_users
):
"""INT-4: Полный жизненный цикл пользователя с LDAP"""
# 1. Регистрация
reg_data = {
"email": "lifecycle.test@test.local",
"first_name": "Lifecycle",
"last_name": "Test",
"phone_number": "+79990007777",
"password": "SecurePass123!",
"whatsapp": "+79990007777",
"birth_date": "1990-01-01",
}
reg_response = api_client.post(
reverse("api:v1:register"),
reg_data,
format="json"
)
assert reg_response.status_code == status.HTTP_201_CREATED
# 2. Верификация email
user = Employee.objects.get(email="lifecycle.test@test.local")
verify_response = api_client.post(
reverse("api:v1:verify-email"),
{
"email": "lifecycle.test@test.local",
"code": user.email_activation_code,
},
format="json"
)
assert verify_response.status_code == status.HTTP_200_OK
# 3. Проверяем финальное состояние
user.refresh_from_db()
assert user.is_active is True
assert user.email_verified is True
# TODO: Проверить состояние в LDAP
# - Пользователь существует
# - Пользователь активен
# - Пароль установлен корректно
@pytest.fixture
def authenticated_client(api_client, db):
"""API клиент с аутентификацией для интеграционных тестов"""
user = Employee.objects.create(
email="admin@test.local",
first_name="Admin",
last_name="User",
phone_number="+79990009999",
is_active=True,
email_verified=True,
is_staff=True,
is_superuser=True,
)
user.set_password("admin123")
user.save()
api_client.force_authenticate(user=user)
return api_client

View File

@ -1,200 +0,0 @@
"""
Простые интеграционные тесты с реальным LDAP сервером.
ВАЖНО: Перед запуском запустите LDAP сервер:
cd backend
./ldap-test.sh start
Запуск тестов:
pytest tests/integration/test_ldap_simple.py -v
"""
import pytest
from rest_framework.test import APIClient
from employees.models import Employee
from tests.integration.conftest import ldap_search_user
@pytest.mark.django_db
def test_register_user_creates_in_ldap(ldap_test_settings, ldap_connection):
"""Регистрация пользователя создает запись в LDAP"""
client = APIClient()
# Arrange
username = "test_newuser"
user_data = {
"username": username,
"email": "test_newuser@test.local",
"password": "TestPassword123!",
"password_confirmation": "TestPassword123!",
"first_name": "Test",
"last_name": "User",
"telegram": "@test_newuser",
"birth_date": "1990-01-01",
"phone_number": "+79001234567",
}
# Act - регистрируем пользователя
response = client.post("/api/v1/auth/register/", user_data)
# Отладка - посмотрим что пришло
print(f"\n Response status: {response.status_code}")
print(f"Response data: {response.data}")
# Assert - проверяем что пользователь создался
assert response.status_code == 201, f"Expected 201, got {response.status_code}: {response.data}"
assert Employee.objects.filter(username=username).exists()
# Assert - проверяем что пользователь появился в LDAP
ldap_user = ldap_search_user(ldap_connection, username)
assert ldap_user is not None, "Пользователь должен быть создан в LDAP"
dn, attrs = ldap_user
assert b"test_newuser@test.local" in attrs.get("mail", [])
@pytest.mark.django_db
def test_verify_email_activates_in_ldap(ldap_test_settings, ldap_connection):
"""Верификация email активирует пользователя в LDAP"""
client = APIClient()
# Arrange - создаем пользователя
username = "test_verify"
user_data = {
"username": username,
"email": "test_verify@test.local",
"password": "TestPassword123!",
"password_confirmation": "TestPassword123!",
"first_name": "Test",
"last_name": "Verify",
}
response = client.post("/api/v1/auth/register/", user_data)
assert response.status_code == 201
# Получаем токен верификации
employee = Employee.objects.get(username=username)
token = employee.email_verification_token
# Act - верифицируем email
verify_response = client.post(
"/api/v1/employees/verify-email/",
{"token": token}
)
# Assert
assert verify_response.status_code == 200
# Проверяем что в LDAP пользователь активен
ldap_user = ldap_search_user(ldap_connection, username)
assert ldap_user is not None
dn, attrs = ldap_user
# TODO: Проверяем что accountStatus = active или аналогичное поле
@pytest.mark.django_db
def test_delete_user_removes_from_ldap(ldap_test_settings, ldap_connection):
"""Удаление пользователя удаляет его из LDAP"""
client = APIClient()
# Arrange - создаем пользователя
username = "test_delete"
user_data = {
"username": username,
"email": "test_delete@test.local",
"password": "TestPassword123!",
"password_confirmation": "TestPassword123!",
"first_name": "Test",
"last_name": "Delete",
}
response = client.post("/api/v1/auth/register/", user_data)
assert response.status_code == 201
# Проверяем что пользователь есть в LDAP
assert ldap_search_user(ldap_connection, username) is not None
# Act - удаляем пользователя
employee = Employee.objects.get(username=username)
employee.delete()
# Assert - проверяем что пользователя нет в LDAP
ldap_user = ldap_search_user(ldap_connection, username)
assert ldap_user is None, "Пользователь должен быть удален из LDAP"
@pytest.mark.django_db
def test_update_user_syncs_to_ldap(ldap_test_settings, ldap_connection):
"""Обновление данных пользователя синхронизируется в LDAP"""
client = APIClient()
# Arrange - создаем пользователя
username = "test_update"
user_data = {
"username": username,
"email": "test_update@test.local",
"password": "TestPassword123!",
"password_confirmation": "TestPassword123!",
"first_name": "Test",
"last_name": "Update",
}
response = client.post("/api/v1/auth/register/", user_data)
assert response.status_code == 201
# Act - обновляем email
employee = Employee.objects.get(username=username)
new_email = "new_email@test.local"
employee.email = new_email
employee.save()
# Assert - проверяем что email обновился в LDAP
ldap_user = ldap_search_user(ldap_connection, username)
assert ldap_user is not None
dn, attrs = ldap_user
assert new_email.encode() in attrs.get("mail", [])
@pytest.mark.django_db
def test_full_user_lifecycle_with_ldap(ldap_test_settings, ldap_connection):
"""Полный жизненный цикл пользователя: создание → верификация → обновление → удаление"""
client = APIClient()
username = "test_lifecycle"
# 1. Создание
user_data = {
"username": username,
"email": "lifecycle@test.local",
"password": "TestPassword123!",
"password_confirmation": "TestPassword123!",
"first_name": "Lifecycle",
"last_name": "Test",
}
response = client.post("/api/v1/auth/register/", user_data)
assert response.status_code == 201
assert ldap_search_user(ldap_connection, username) is not None
# 2. Верификация
employee = Employee.objects.get(username=username)
token = employee.email_verification_token
verify_response = client.post(
"/api/v1/employees/verify-email/",
{"token": token}
)
assert verify_response.status_code == 200
# 3. Обновление
employee.refresh_from_db()
employee.first_name = "Updated"
employee.save()
ldap_user = ldap_search_user(ldap_connection, username)
assert ldap_user is not None
# 4. Удаление
employee.delete()
assert ldap_search_user(ldap_connection, username) is None

View File

@ -1,44 +0,0 @@
# Базовая структура OU для тестов
dn: ou=Users,dc=test,dc=local
objectClass: organizationalUnit
ou: Users
description: Container for test users
dn: ou=Groups,dc=test,dc=local
objectClass: organizationalUnit
ou: Groups
description: Container for test groups
dn: ou=Departments,dc=test,dc=local
objectClass: organizationalUnit
ou: Departments
description: Container for test departments
# Тестовый пользователь для начальной проверки
dn: cn=test.user,ou=Users,dc=test,dc=local
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: top
cn: test.user
sn: User
givenName: Test
uid: test.user
uidNumber: 10001
gidNumber: 10001
homeDirectory: /home/test.user
mail: test.user@test.local
userPassword: {SSHA}test123
description: Test user for LDAP integration tests
# Тестовая группа
dn: cn=test-group,ou=Groups,dc=test,dc=local
objectClass: groupOfNames
cn: test-group
description: Test group for integration tests
member: cn=test.user,ou=Users,dc=test,dc=local
# Тестовый отдел
dn: ou=IT,ou=Departments,dc=test,dc=local
objectClass: organizationalUnit
ou: IT
description: IT Department for testing

View File

@ -1,8 +0,0 @@
"""
Конфигурация для manual тестов.
Все тесты в этой директории автоматически помечаются как manual.
"""
import pytest
# Автоматически помечать все тесты в этой директории как manual
pytestmark = pytest.mark.manual

View File

@ -1,84 +0,0 @@
#!/usr/bin/env python
"""
Тестирование уведомлений для объявлений
"""
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
django.setup()
from communications.models import Chat, Message
from employees.models import Employee
def test_announcement_notifications():
"""Проверяет, что уведомления для объявлений отправляются всем"""
# Находим чат-объявление
announcement = Chat.objects.filter(type='announcement').first()
if not announcement:
print("❌ Нет объявлений в системе")
return
print(f"✓ Найдено объявление: {announcement.name}")
print(f" Created by: {announcement.created_by}")
print(f" include_all_employees: {announcement.include_all_employees}")
# Получаем участников
participants = announcement.get_participants
print(f" Участников (get_participants): {participants.count()}")
# Список участников
for p in participants[:5]:
print(f" - {p.get_full_name() or p.username} ({p.email})")
if participants.count() > 5:
print(f" ... и ещё {participants.count() - 5}")
# Проверяем последнее сообщение
last_message = announcement.messages.order_by('-created_at').first()
if last_message:
print(f"\n✓ Последнее сообщение:")
print(f" ID: {last_message.id}")
print(f" Автор: {last_message.author}")
print(f" Текст: {last_message.content[:50]}...")
print(f" Создано: {last_message.created_at}")
# Проверяем уведомления
from notifications.models import Notification
notifications = Notification.objects.filter(
content_type__model='message',
object_id=last_message.id
)
print(f"\n✓ Уведомлений создано: {notifications.count()}")
# Проверяем тип уведомлений
announcement_notifs = notifications.filter(
notification_type__code='announcement_new_message'
)
print(f" Типа 'announcement_new_message': {announcement_notifs.count()}")
# Показываем первые 5
for n in notifications[:5]:
print(f" - Для: {n.recipient.get_full_name()}")
print(f" Тип: {n.notification_type.code}")
print(f" Прочитано: {n.is_read}")
else:
print("\n❌ Нет сообщений в объявлении")
# Итоги
print("\n" + "="*60)
all_employees = Employee.objects.filter(is_active=True).count()
print(f"Всего активных сотрудников: {all_employees}")
print(f"Участников объявления: {participants.count()}")
if participants.count() == all_employees:
print("✅ ПРАВИЛЬНО: Все сотрудники получат уведомления")
else:
print(f"⚠️ ВНИМАНИЕ: Уведомления получат только {participants.count()} из {all_employees}")
if __name__ == '__main__':
test_announcement_notifications()

View File

@ -1,168 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Тестовый скрипт для проверки функции compress_avatar (система).
Упрощённая версия без эмодзи для Windows консоли.
Использование:
python test_avatar_simple.py <путь_к_файлу>
"""
import os
import sys
from pathlib import Path
# Добавляем путь к модулям Django
sys.path.insert(0, str(Path(__file__).parent))
# Настраиваем Django перед импортом
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
import django
django.setup()
try:
from PIL import Image
except ImportError:
print("ERROR: Pillow не установлен. Установите: pip install Pillow")
sys.exit(1)
from common.image_utils import compress_avatar
def get_image_info(image_path):
"""Получить информацию об изображении."""
try:
img = Image.open(image_path)
size = os.path.getsize(image_path)
# EXIF ориентация
orientation_code = None
orientation_name = "No EXIF"
try:
exif = img.getexif()
if exif:
orientation_code = exif.get(0x0112)
orientation_names = {
1: "Normal", 3: "Rotated 180",
6: "Rotated 90 CW", 8: "Rotated 270 CW"
}
orientation_name = orientation_names.get(orientation_code, f"Code {orientation_code}")
except:
pass
return {
'size': size,
'size_kb': size / 1024,
'dimensions': img.size,
'mode': img.mode,
'format': img.format,
'orientation_code': orientation_code,
'orientation_name': orientation_name
}
except Exception as e:
return {'error': str(e)}
def test_avatar_compression(input_path):
"""Тестировать сжатие аватара."""
print("\n" + "="*70)
print(f"Avatar Compression Test: {os.path.basename(input_path)}")
print("="*70)
# Информация ДО
print("\nBEFORE processing:")
info_before = get_image_info(input_path)
if 'error' in info_before:
print(f" ERROR: {info_before['error']}")
return False
print(f" Size: {info_before['size_kb']:.2f} KB ({info_before['size']:,} bytes)")
print(f" Resolution: {info_before['dimensions'][0]}x{info_before['dimensions'][1]}")
print(f" Mode: {info_before['mode']}, Format: {info_before['format']}")
print(f" EXIF Orientation: {info_before['orientation_name']}")
# Обработка
try:
with open(input_path, 'rb') as f:
original_data = f.read()
print("\nProcessing with compress_avatar(max_width=512, max_kb=500)...")
compressed_data = compress_avatar(original_data)
# Сохранение
output_path = input_path.replace('.', '_compressed.')
if not output_path.endswith('.jpg'):
output_path = os.path.splitext(output_path)[0] + '.jpg'
with open(output_path, 'wb') as f:
f.write(compressed_data)
print(f" Saved to: {output_path}")
except Exception as e:
print(f" ERROR: {e}")
import traceback
traceback.print_exc()
return False
# Информация ПОСЛЕ
print("\nAFTER processing:")
info_after = get_image_info(output_path)
if 'error' in info_after:
print(f" ERROR: {info_after['error']}")
return False
print(f" Size: {info_after['size_kb']:.2f} KB ({info_after['size']:,} bytes)")
print(f" Resolution: {info_after['dimensions'][0]}x{info_after['dimensions'][1]}")
print(f" Mode: {info_after['mode']}, Format: {info_after['format']}")
print(f" EXIF Orientation: {info_after['orientation_name']}")
# Статистика
size_reduction = ((info_before['size'] - info_after['size']) / info_before['size']) * 100
print(f"\nStatistics:")
print(f" Size reduction: {size_reduction:.1f}%")
print(f" Before: {info_before['dimensions'][0]}x{info_before['dimensions'][1]} ({info_before['size_kb']:.2f} KB)")
print(f" After: {info_after['dimensions'][0]}x{info_after['dimensions'][1]} ({info_after['size_kb']:.2f} KB)")
# Проверка ориентации
if info_before['orientation_code'] and info_before['orientation_code'] != 1:
if info_after['orientation_code'] is None or info_after['orientation_code'] == 1:
print(f" [OK] Orientation fixed automatically!")
print(f" Was: {info_before['orientation_name']}")
print(f" Now: Normal (rotated in pixels)")
else:
print(f" [WARN] EXIF orientation preserved: {info_after['orientation_name']}")
print("="*70)
return True
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: python test_avatar_simple.py <image_file>")
print("\nExample:")
print(" python test_avatar_simple.py photo.jpg")
sys.exit(1)
input_path = sys.argv[1]
if not os.path.exists(input_path):
print(f"ERROR: File not found: {input_path}")
sys.exit(1)
if not os.path.isfile(input_path):
print(f"ERROR: Not a file: {input_path}")
sys.exit(1)
try:
success = test_avatar_compression(input_path)
sys.exit(0 if success else 1)
except KeyboardInterrupt:
print("\n\nInterrupted by user")
sys.exit(130)
except Exception as e:
print(f"\nCRITICAL ERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@ -1,52 +0,0 @@
#!/usr/bin/env python
"""Проверка разрешения IP для корпоративной сети 172.11.0.0/16"""
import os
import sys
sys.path.insert(0, os.path.dirname(__file__))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
import django
django.setup()
from common.ip_restrictions import is_ip_allowed
print("\n" + "=" * 60)
print("Проверка IP-адресов для корпоративной сети 172.11.0.0/16")
print("=" * 60)
test_ips = [
('172.11.0.117', True, 'Ваш текущий IP'),
('172.11.0.1', True, 'Корпоративная сеть 172.11.x.x'),
('172.11.255.255', True, 'Край диапазона 172.11.x.x'),
('172.12.0.1', False, 'За пределами 172.11.x.x'),
('172.16.0.1', True, 'Стандартная приватная сеть'),
('192.168.1.1', True, 'Локальная сеть'),
('10.0.0.1', True, 'Приватная сеть класса A'),
('8.8.8.8', False, 'Публичный IP (Google DNS)'),
]
passed = 0
failed = 0
for ip, expected, description in test_ips:
result = is_ip_allowed(ip)
status = "✅" if result == expected else "❌"
if result == expected:
passed += 1
else:
failed += 1
result_text = "разрешен" if result else "заблокирован"
expected_text = "разрешен" if expected else "заблокирован"
print(f"{status} {ip:20} -> {result_text:12} "
f"(ожидалось: {expected_text:12}) # {description}")
print("\n" + "=" * 60)
if failed == 0:
print(f"✅ Все тесты пройдены: {passed}/{passed + failed}")
else:
print(f"⚠️ Тестов пройдено: {passed}/{passed + failed}, провалено: {failed}")
print("=" * 60 + "\n")

View File

@ -1,81 +0,0 @@
#!/usr/bin/env python
"""
Создаём тестовое сообщение в объявлении и проверяем уведомления
"""
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
django.setup()
from communications.models import Chat, Message
from employees.models import Employee
from notifications.models import Notification
def create_test_message():
"""Создаёт тестовое сообщение в объявлении"""
# Находим чат-объявление
announcement = Chat.objects.filter(type='announcement').first()
if not announcement:
print("❌ Нет объявлений в системе")
return
print(f"✓ Найдено объявление: {announcement.name}")
print(f" Created by: {announcement.created_by}")
# Подсчитываем уведомления ДО
notif_count_before = Notification.objects.count()
print(f"\nУведомлений ДО создания сообщения: {notif_count_before}")
# Создаём сообщение
message = Message.objects.create(
chat=announcement,
author=announcement.created_by,
content="ТЕСТОВОЕ ОБЪЯВЛЕНИЕ для проверки уведомлений"
)
print(f"\n✓ Создано сообщение ID={message.id}")
# Подсчитываем уведомления ПОСЛЕ
notif_count_after = Notification.objects.count()
print(f"Уведомлений ПОСЛЕ создания сообщения: {notif_count_after}")
print(f"Создано новых уведомлений: {notif_count_after - notif_count_before}")
# Проверяем уведомления для этого сообщения
message_notifs = Notification.objects.filter(
content_type__model='message',
object_id=message.id
)
print(f"\nУведомлений для сообщения ID={message.id}: {message_notifs.count()}")
if message_notifs.exists():
print("\n📧 Список уведомлений:")
for n in message_notifs:
print(f" - Получатель: {n.recipient.get_full_name()}")
print(f" Тип: {n.notification_type.code}")
print(f" Заголовок: {n.title}")
print()
else:
print("\n❌ Уведомления НЕ созданы!")
print("\nПроверяем настройки:")
print(f" - message.is_system: {message.is_system}")
print(f" - message.is_deleted: {message.is_deleted}")
print(f" - chat.type: {announcement.type}")
# Проверяем участников
participants = announcement.get_participants
print(f" - Участников: {participants.count()}")
for p in participants:
print(f" * {p.get_full_name()} (ID={p.id})")
print(f" - Автор: {message.author.get_full_name()} (ID={message.author.id})")
# Участники без автора
participants_no_author = participants.exclude(id=message.author.id)
print(f" - Участников без автора: {participants_no_author.count()}")
if __name__ == '__main__':
create_test_message()

View File

@ -1,114 +0,0 @@
#!/usr/bin/env python3
"""
Тестовый скрипт для проверки отправки email через SMTP Яндекса
"""
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
# Настройки из .env
EMAIL_HOST = "smtp.yandex.ru"
EMAIL_PORT = 465
EMAIL_USE_SSL = True
EMAIL_HOST_USER = "robotail-info@yandex.ru"
EMAIL_HOST_PASSWORD = "wogrpijpzbtiuptx"
DEFAULT_FROM_EMAIL = "Robotail <robotail-info@yandex.ru>"
# Получатель
TO_EMAIL = "igor_26reg@mail.ru"
def test_email():
print("=" * 50)
print("Тест отправки email через Яндекс SMTP")
print("=" * 50)
print(f"Хост: {EMAIL_HOST}:{EMAIL_PORT}")
print(f"От кого: {EMAIL_HOST_USER}")
print(f"Кому: {TO_EMAIL}")
print(f"SSL: {EMAIL_USE_SSL}")
print()
# Создаем сообщение
msg = MIMEMultipart('alternative')
msg['Subject'] = "Тестовое письмо - проверка SMTP"
msg['From'] = DEFAULT_FROM_EMAIL
msg['To'] = TO_EMAIL
# Текст письма
text = "Это тестовое письмо для проверки настроек SMTP."
html = """
<html>
<head></head>
<body>
<h2>Тестовое письмо</h2>
<p>Это тестовое письмо для проверки настроек SMTP Яндекса.</p>
<p>Если вы получили это письмо, значит настройки корректны.</p>
</body>
</html>
"""
part1 = MIMEText(text, 'plain', 'utf-8')
part2 = MIMEText(html, 'html', 'utf-8')
msg.attach(part1)
msg.attach(part2)
try:
print("Подключение к SMTP серверу...")
if EMAIL_USE_SSL:
server = smtplib.SMTP_SSL(EMAIL_HOST, EMAIL_PORT, timeout=10)
else:
server = smtplib.SMTP(EMAIL_HOST, EMAIL_PORT, timeout=10)
server.starttls()
print("✓ Подключение установлено")
print("Авторизация...")
server.login(EMAIL_HOST_USER, EMAIL_HOST_PASSWORD)
print("✓ Авторизация успешна")
print("Отправка письма...")
server.send_message(msg)
print("✓ Письмо отправлено!")
server.quit()
print()
print("=" * 50)
print("SUCCESS! Письмо успешно отправлено на", TO_EMAIL)
print("=" * 50)
except smtplib.SMTPAuthenticationError as e:
print()
print("=" * 50)
print("ОШИБКА АВТОРИЗАЦИИ!")
print("=" * 50)
print(f"Код ошибки: {e.smtp_code}")
print(f"Сообщение: {e.smtp_error.decode('utf-8')}")
print()
print("Возможные причины:")
print("1. Неверный пароль приложения")
print("2. Доступ заблокирован для данного IP-адреса")
print("3. Необходимо создать новый пароль приложения в настройках Яндекса")
print()
print("Решение:")
print("1. Зайдите на https://passport.yandex.ru/profile")
print("2. Перейдите в раздел 'Пароли и авторизация'")
print("3. Создайте новый пароль приложения")
print("4. Обновите EMAIL_HOST_PASSWORD в .env файле")
except smtplib.SMTPException as e:
print()
print("=" * 50)
print("ОШИБКА SMTP!")
print("=" * 50)
print(f"Ошибка: {e}")
except Exception as e:
print()
print("=" * 50)
print("ОБЩАЯ ОШИБКА!")
print("=" * 50)
print(f"Ошибка: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
test_email()

View File

@ -1,171 +0,0 @@
#!/usr/bin/env python
"""
Тестирование IP-ограничений для регистрации.
Запуск:
python backend/test_ip_restrictions.py
"""
import os
import sys
import django
# Настройка Django
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
os.environ.setdefault("DJANGO_SECRET_KEY", "test-key")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "eusrr_backend.settings")
django.setup()
from common.ip_restrictions import (
get_client_ip,
is_local_ip,
is_ip_allowed,
)
def test_is_local_ip():
"""Тест проверки локальных IP."""
print("\n=== Тест: is_local_ip ===")
test_cases = [
('127.0.0.1', True, 'localhost IPv4'),
('127.0.0.100', True, 'localhost range'),
('192.168.1.1', True, 'private class C'),
('192.168.100.50', True, 'private class C'),
('10.0.0.1', True, 'private class A'),
('172.16.0.1', True, 'private class B'),
('172.31.255.255', True, 'private class B edge'),
('8.8.8.8', False, 'public Google DNS'),
('203.0.113.50', False, 'public TEST-NET'),
('::1', True, 'localhost IPv6'),
('invalid-ip', False, 'invalid IP'),
]
passed = 0
failed = 0
for ip, expected, description in test_cases:
result = is_local_ip(ip)
status = "✅" if result == expected else "❌"
if result == expected:
passed += 1
else:
failed += 1
print(f"{status} {ip:20} -> {result:5} (expected: {expected:5}) # {description}")
print(f"\nПройдено: {passed}/{passed + failed}")
return failed == 0
def test_is_ip_allowed():
"""Тест проверки разрешенных IP с кастомными настройками."""
print("\n=== Тест: is_ip_allowed ===")
# Тест 1: Разрешить все
print("\n1. Разрешить все IP ['*']:")
result = is_ip_allowed('203.0.113.50', ['*'])
print(f" {'✅' if result else '❌'} 203.0.113.50 -> {result} (expected: True)")
# Тест 2: Разрешить конкретную сеть
print("\n2. Разрешить сеть 192.168.1.0/24:")
allowed_net = ['192.168.1.0/24']
tests = [
('192.168.1.1', True),
('192.168.1.100', True),
('192.168.1.255', True),
('192.168.2.1', False),
('10.0.0.1', False),
]
for ip, expected in tests:
result = is_ip_allowed(ip, allowed_net)
status = "✅" if result == expected else "❌"
print(f" {status} {ip:20} -> {result:5} (expected: {expected:5})")
# Тест 3: Разрешить конкретные IP
print("\n3. Разрешить конкретные IP ['192.168.1.100', '10.0.0.5']:")
allowed_ips = ['192.168.1.100', '10.0.0.5']
tests = [
('192.168.1.100', True),
('10.0.0.5', True),
('192.168.1.101', False),
('10.0.0.6', False),
]
for ip, expected in tests:
result = is_ip_allowed(ip, allowed_ips)
status = "✅" if result == expected else "❌"
print(f" {status} {ip:20} -> {result:5} (expected: {expected:5})")
# Тест 4: По умолчанию (только локальные)
print("\n4. По умолчанию (None = только локальные IP):")
tests = [
('127.0.0.1', True),
('192.168.1.1', True),
('10.0.0.1', True),
('8.8.8.8', False),
('203.0.113.50', False),
]
for ip, expected in tests:
result = is_ip_allowed(ip, None)
status = "✅" if result == expected else "❌"
print(f" {status} {ip:20} -> {result:5} (expected: {expected:5})")
def test_get_client_ip():
"""Тест получения IP из request."""
print("\n=== Тест: get_client_ip ===")
from django.test import RequestFactory
factory = RequestFactory()
# Тест 1: Обычный запрос
print("\n1. Обычный запрос (без прокси):")
request = factory.get('/')
ip = get_client_ip(request)
print(f" IP: {ip}")
# Тест 2: С заголовком X-Forwarded-For
print("\n2. Запрос через прокси (X-Forwarded-For):")
request = factory.get('/', HTTP_X_FORWARDED_FOR='203.0.113.50, 192.168.1.1')
ip = get_client_ip(request)
expected = '203.0.113.50'
status = "✅" if ip == expected else "❌"
print(f" {status} IP: {ip} (expected: {expected})")
# Тест 3: С одним IP в X-Forwarded-For
print("\n3. Запрос через прокси (один IP):")
request = factory.get('/', HTTP_X_FORWARDED_FOR='10.0.0.5')
ip = get_client_ip(request)
expected = '10.0.0.5'
status = "✅" if ip == expected else "❌"
print(f" {status} IP: {ip} (expected: {expected})")
def main():
"""Запуск всех тестов."""
print("=" * 60)
print("Тестирование модуля IP-ограничений")
print("=" * 60)
try:
test_is_local_ip()
test_is_ip_allowed()
test_get_client_ip()
print("\n" + "=" * 60)
print("✅ Все тесты выполнены")
print("=" * 60)
except Exception as e:
print(f"\n❌ Ошибка: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -1,167 +0,0 @@
#!/usr/bin/env python
"""
Простой тест IP-ограничений (без Django).
"""
import ipaddress
def is_local_ip(ip_address: str) -> bool:
"""Проверяет, является ли IP-адрес локальным."""
try:
ip = ipaddress.ip_address(ip_address)
return ip.is_private or ip.is_loopback or ip.is_link_local
except ValueError:
return False
def is_ip_allowed(ip_address: str, allowed_networks=None):
"""Проверяет, разрешен ли доступ с данного IP-адреса."""
if allowed_networks is None:
return is_local_ip(ip_address)
if not allowed_networks:
return False
if allowed_networks == ['*'] or '*' in allowed_networks:
return True
try:
ip = ipaddress.ip_address(ip_address)
for network_str in allowed_networks:
if '/' in network_str:
network = ipaddress.ip_network(network_str, strict=False)
if ip in network:
return True
else:
allowed_ip = ipaddress.ip_address(network_str)
if ip == allowed_ip:
return True
return False
except ValueError:
return False
def test_is_local_ip():
"""Тест проверки локальных IP."""
print("\n=== Тест: is_local_ip ===")
test_cases = [
('127.0.0.1', True, 'localhost IPv4'),
('127.0.0.100', True, 'localhost range'),
('192.168.1.1', True, 'private class C'),
('192.168.100.50', True, 'private class C'),
('10.0.0.1', True, 'private class A'),
('172.16.0.1', True, 'private class B'),
('172.31.255.255', True, 'private class B edge'),
('8.8.8.8', False, 'public Google DNS'),
('203.0.113.50', False, 'public TEST-NET'),
('::1', True, 'localhost IPv6'),
('invalid-ip', False, 'invalid IP'),
]
passed = 0
failed = 0
for ip, expected, description in test_cases:
result = is_local_ip(ip)
status = "✅" if result == expected else "❌"
if result == expected:
passed += 1
else:
failed += 1
print(f"{status} {ip:20} -> {str(result):5} "
f"(expected: {str(expected):5}) # {description}")
print(f"\nПройдено: {passed}/{passed + failed}")
return failed == 0
def test_is_ip_allowed():
"""Тест проверки разрешенных IP с кастомными настройками."""
print("\n=== Тест: is_ip_allowed ===")
# Тест 1: Разрешить все
print("\n1. Разрешить все IP ['*']:")
result = is_ip_allowed('203.0.113.50', ['*'])
status = '✅' if result else '❌'
print(f" {status} 203.0.113.50 -> {result} (expected: True)")
# Тест 2: Разрешить конкретную сеть
print("\n2. Разрешить сеть 192.168.1.0/24:")
allowed_net = ['192.168.1.0/24']
tests = [
('192.168.1.1', True),
('192.168.1.100', True),
('192.168.1.255', True),
('192.168.2.1', False),
('10.0.0.1', False),
]
for ip, expected in tests:
result = is_ip_allowed(ip, allowed_net)
status = "✅" if result == expected else "❌"
print(f" {status} {ip:20} -> {str(result):5} "
f"(expected: {str(expected):5})")
# Тест 3: Разрешить конкретные IP
print("\n3. Разрешить IP ['192.168.1.100', '10.0.0.5']:")
allowed_ips = ['192.168.1.100', '10.0.0.5']
tests = [
('192.168.1.100', True),
('10.0.0.5', True),
('192.168.1.101', False),
('10.0.0.6', False),
]
for ip, expected in tests:
result = is_ip_allowed(ip, allowed_ips)
status = "✅" if result == expected else "❌"
print(f" {status} {ip:20} -> {str(result):5} "
f"(expected: {str(expected):5})")
# Тест 4: По умолчанию (только локальные)
print("\n4. По умолчанию (None = только локальные IP):")
tests = [
('127.0.0.1', True),
('192.168.1.1', True),
('10.0.0.1', True),
('8.8.8.8', False),
('203.0.113.50', False),
]
for ip, expected in tests:
result = is_ip_allowed(ip, None)
status = "✅" if result == expected else "❌"
print(f" {status} {ip:20} -> {str(result):5} "
f"(expected: {str(expected):5})")
def main():
"""Запуск всех тестов."""
print("=" * 60)
print("Тестирование модуля IP-ограничений")
print("=" * 60)
try:
test_is_local_ip()
test_is_ip_allowed()
print("\n" + "=" * 60)
print("✅ Все тесты выполнены успешно")
print("=" * 60)
except Exception as e:
print(f"\n❌ Ошибка: {e}")
import traceback
traceback.print_exc()
return 1
return 0
if __name__ == "__main__":
exit(main())

View File

@ -1,176 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Тестовый скрипт для проверки функции normalize_avatar_to_jpeg (LDAP).
Упрощённая версия без эмодзи для Windows консоли.
Использование:
python test_ldap_simple.py <путь_к_файлу>
"""
import os
import sys
from pathlib import Path
# Добавляем путь к модулям Django
sys.path.insert(0, str(Path(__file__).parent))
# Настраиваем Django перед импортом
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
import django
django.setup()
try:
from PIL import Image
except ImportError:
print("ERROR: Pillow не установлен. Установите: pip install Pillow")
sys.exit(1)
from employees.ldap.utils.image_utils import normalize_avatar_to_jpeg
def get_image_info(image_path):
"""Получить информацию об изображении."""
try:
img = Image.open(image_path)
size = os.path.getsize(image_path)
# EXIF ориентация
orientation_code = None
orientation_name = "No EXIF"
try:
exif = img.getexif()
if exif:
orientation_code = exif.get(0x0112)
orientation_names = {
1: "Normal", 3: "Rotated 180",
6: "Rotated 90 CW", 8: "Rotated 270 CW"
}
orientation_name = orientation_names.get(orientation_code, f"Code {orientation_code}")
except:
pass
return {
'size': size,
'size_kb': size / 1024,
'dimensions': img.size,
'mode': img.mode,
'format': img.format,
'orientation_code': orientation_code,
'orientation_name': orientation_name
}
except Exception as e:
return {'error': str(e)}
def test_ldap_compression(input_path):
"""Тестировать LDAP-сжатие одного файла."""
print("\n" + "="*70)
print(f"LDAP Image Compression Test: {os.path.basename(input_path)}")
print("="*70)
# Информация ДО
print("\nBEFORE processing:")
info_before = get_image_info(input_path)
if 'error' in info_before:
print(f" ERROR: {info_before['error']}")
return False
print(f" Size: {info_before['size_kb']:.2f} KB")
print(f" Resolution: {info_before['dimensions'][0]}x{info_before['dimensions'][1]}")
print(f" Mode: {info_before['mode']}, Format: {info_before['format']}")
print(f" EXIF Orientation: {info_before['orientation_name']}")
# Обработка
try:
with open(input_path, 'rb') as f:
original_data = f.read()
print("\nProcessing with normalize_avatar_to_jpeg(size_px=384, max_kb=100)...")
print(" HIGH QUALITY: progressive JPEG, 4:4:4 subsampling, quality 75-98")
compressed_data = normalize_avatar_to_jpeg(
original_data, size_px=384, max_kb=100
)
# Сохранение
output_path = input_path.replace('.', '_ldap.')
if not output_path.endswith('.jpg'):
output_path = os.path.splitext(output_path)[0] + '.jpg'
with open(output_path, 'wb') as f:
f.write(compressed_data)
print(f" Saved to: {output_path}")
except Exception as e:
print(f" ERROR: {e}")
import traceback
traceback.print_exc()
return False
# Информация ПОСЛЕ
print("\nAFTER processing (LDAP format):")
info_after = get_image_info(output_path)
if 'error' in info_after:
print(f" ERROR: {info_after['error']}")
return False
print(f" Size: {info_after['size_kb']:.2f} KB")
print(f" Resolution: {info_after['dimensions'][0]}x{info_after['dimensions'][1]}")
print(f" Mode: {info_after['mode']}, Format: {info_after['format']}")
print(f" EXIF Orientation: {info_after['orientation_name']}")
# Проверка лимитов
size_ok = info_after['size_kb'] <= 100
print(f"\nLDAP Compliance Check:")
print(f" [{'OK' if size_ok else 'FAIL'}] File size: {info_after['size_kb']:.2f} KB (limit: 100 KB)")
# Статистика
size_reduction = ((info_before['size'] - info_after['size']) / info_before['size']) * 100
print(f"\nStatistics:")
print(f" Size reduction: {size_reduction:.1f}%")
print(f" Before: {info_before['dimensions'][0]}x{info_before['dimensions'][1]} ({info_before['size_kb']:.2f} KB)")
print(f" After: {info_after['dimensions'][0]}x{info_after['dimensions'][1]} ({info_after['size_kb']:.2f} KB)")
# Проверка ориентации
if info_before['orientation_code'] and info_before['orientation_code'] != 1:
if info_after['orientation_code'] is None or info_after['orientation_code'] == 1:
print(f" [OK] Orientation fixed automatically!")
print(f" Was: {info_before['orientation_name']}")
print(f" Now: Normal (rotated in pixels)")
else:
print(f" [WARN] EXIF orientation preserved: {info_after['orientation_name']}")
print("="*70)
return True
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: python test_ldap_simple.py <image_file>")
print("\nExample:")
print(" python test_ldap_simple.py photo.jpg")
sys.exit(1)
input_path = sys.argv[1]
if not os.path.exists(input_path):
print(f"ERROR: File not found: {input_path}")
sys.exit(1)
if not os.path.isfile(input_path):
print(f"ERROR: Not a file: {input_path}")
sys.exit(1)
try:
success = test_ldap_compression(input_path)
sys.exit(0 if success else 1)
except KeyboardInterrupt:
print("\n\nInterrupted by user")
sys.exit(130)
except Exception as e:
print(f"\nCRITICAL ERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@ -1,43 +0,0 @@
#!/usr/bin/env python
"""
Скрипт для создания тестового уведомления
"""
import os
import sys
import django
# Настройка Django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
django.setup()
from employees.models import Employee
from notifications.services import NotificationService
def main():
"""Создать тестовое уведомление"""
# Получаем первого пользователя
user = Employee.objects.filter(id=1).first()
if not user:
print("❌ Нет пользователей в системе!")
sys.exit(1)
# Создаём тестовое уведомление
notification = NotificationService.create_notification(
recipient=user,
notification_type_code='system_announcement',
title='🎉 Тестовое уведомление',
message='Проверка real-time уведомлений через WebSocket. Вы должны увидеть toast справа вверху!',
action_url='/',
)
print(f'✅ Создано уведомление ID={notification.id}')
print(f'👤 Получатель: {user.last_name} {user.first_name}')
print(f'📧 Отправлено на веб: {notification.sent_web}')
print(f'📝 Приоритет: {notification.notification_type.priority}')
print(f'🔔 Категория: {notification.notification_type.category.name}')
print()
print('Проверьте браузер - должен появиться toast и обновиться badge!')
if __name__ == '__main__':
main()

View File

@ -1,95 +0,0 @@
#!/usr/bin/env python
"""
Скрипт для отладки уведомлений
"""
import os
import sys
import django
import logging
# Включаем подробное логирование
logging.basicConfig(level=logging.DEBUG)
# Настройка Django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
django.setup()
from employees.models import Employee
from notifications.services import NotificationService
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
def main():
"""Создать тестовое уведомление с отладкой"""
print("=" * 60)
print("ТЕСТ СИСТЕМЫ УВЕДОМЛЕНИЙ")
print("=" * 60)
# Проверка Channel Layer
channel_layer = get_channel_layer()
print(f"\n1. Channel Layer: {type(channel_layer).__name__}")
# Получаем пользователя
user = Employee.objects.first()
if not user:
print("❌ Нет пользователей в системе!")
sys.exit(1)
print(f"\n2. Получатель: {user.last_name} {user.first_name} (ID={user.id})")
print(f" Email: {user.email}")
# Создаём уведомление
print("\n3. Создание уведомления...")
notification = NotificationService.create_notification(
recipient=user,
notification_type_code='system_announcement',
title='🔔 Тестовое уведомление',
message='Это тестовое сообщение для проверки WebSocket. Если вы видите это в браузере - система работает!',
action_url='/',
)
if not notification:
print("❌ Не удалось создать уведомление!")
sys.exit(1)
print(f"✅ Уведомление создано: ID={notification.id}")
print(f" Заголовок: {notification.title}")
print(f" Отправлено веб: {notification.sent_web}")
print(f" Дата отправки: {notification.sent_at}")
# Проверка отправки через WebSocket
print(f"\n4. WebSocket группа: notifications_{user.id}")
# Попробуем отправить напрямую
print("\n5. Отправка тестового сообщения через Channel Layer...")
try:
async_to_sync(channel_layer.group_send)(
f'notifications_{user.id}',
{
'type': 'notification_new',
'notification': {
'id': notification.id,
'title': '🧪 Прямое тестовое сообщение',
'message': 'Это сообщение отправлено напрямую через Channel Layer',
'category': 'system',
'icon': 'bi-gear',
'color': 'light',
'priority': 'high',
}
}
)
print("✅ Сообщение отправлено в Channel Layer")
except Exception as e:
print(f"❌ Ошибка отправки: {e}")
print("\n" + "=" * 60)
print("ПРОВЕРЬТЕ БРАУЗЕР!")
print("=" * 60)
print("Вы должны увидеть:")
print(" 1. Toast уведомление справа вверху")
print(" 2. Badge с цифрой в navbar")
print(" 3. В консоли: [Notifications] Received: ...")
print("=" * 60)
if __name__ == '__main__':
main()

View File

@ -1,428 +0,0 @@
#!/usr/bin/env python
"""
Тестовый скрипт для проверки работы notification signals.
Тестирует все 5 модулей:
1. Communications - сообщения, упоминания, ответы
2. Requests - заявки, статусы, комментарии
3. Documents - документы, ознакомления
4. Calendar - события, изменения
5. Feed - публикации, комментарии
Запуск:
python test_notification_signals.py
"""
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
django.setup()
from django.contrib.auth import get_user_model
from communications.models import Chat, Message
from requests_app.models import Request, RequestComment
from documents.models import Document, DocumentAcknowledgement
from calendar_app.models import CalendarEvent
from feed.models import Post, Comment
from notifications.models import Notification
from employees.models import Department
Employee = get_user_model()
# ANSI цвета для красивого вывода
class Colors:
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
def print_header(text):
print(f"\n{Colors.HEADER}{Colors.BOLD}{'='*60}{Colors.ENDC}")
print(f"{Colors.HEADER}{Colors.BOLD}{text:^60}{Colors.ENDC}")
print(f"{Colors.HEADER}{Colors.BOLD}{'='*60}{Colors.ENDC}\n")
def print_success(text):
print(f"{Colors.OKGREEN}[OK] {text}{Colors.ENDC}")
def print_error(text):
print(f"{Colors.FAIL}[ERROR] {text}{Colors.ENDC}")
def print_info(text):
print(f"{Colors.OKCYAN}[INFO] {text}{Colors.ENDC}")
def get_or_create_test_users():
"""Создает или получает тестовых пользователей."""
print_info("Подготовка тестовых пользователей...")
user1, created = Employee.objects.get_or_create(
email='test_user1@test.com',
defaults={
'first_name': 'Иван',
'last_name': 'Иванов',
'is_active': True,
'phone_number': '+79001111111'
}
)
if created:
user1.set_password('testpass123')
user1.save()
user2, created = Employee.objects.get_or_create(
email='test_user2@test.com',
defaults={
'first_name': 'Петр',
'last_name': 'Петров',
'is_active': True,
'phone_number': '+79002222222'
}
)
if created:
user2.set_password('testpass123')
user2.save()
print_success(f"Пользователи готовы: {user1.email}, {user2.email}")
return user1, user2
def test_communications(user1, user2):
"""Тестирует signals модуля Communications."""
print_header("ТЕСТ 1: Communications")
# Очистка предыдущих уведомлений
Notification.objects.filter(recipient__in=[user1, user2]).delete()
# 1. Создаем приватный чат
chat = Chat.objects.create(type='private')
chat.participants.add(user1, user2)
print_success(f"Создан чат #{chat.id}")
# Проверяем уведомление о добавлении в чат
notif_count = Notification.objects.filter(
notification_type__code='chat_added_to_chat'
).count()
print_info(f"Уведомлений о добавлении в чат: {notif_count}")
# 2. Простое сообщение
msg1 = Message.objects.create(
chat=chat,
author=user1,
content='Привет! Как дела?'
)
print_success(f"Создано сообщение #{msg1.id}")
notif = Notification.objects.filter(
recipient=user2,
notification_type__code='chat_new_message'
).first()
if notif:
print_success(f"[+] Уведомление получено: {notif.title}")
else:
print_error("Уведомление о новом сообщении НЕ создано!")
# 3. Сообщение с упоминанием
msg2 = Message.objects.create(
chat=chat,
author=user1,
content=f'Привет @{user2.email}, нужна твоя помощь!'
)
print_success(f"Создано сообщение с упоминанием #{msg2.id}")
mention_notif = Notification.objects.filter(
recipient=user2,
notification_type__code='chat_mention'
).first()
if mention_notif:
print_success(f"[+] Уведомление об упоминании: {mention_notif.title}")
else:
print_error("Уведомление об упоминании НЕ создано!")
# 4. Ответ на сообщение
msg3 = Message.objects.create(
chat=chat,
author=user2,
content='Конечно, помогу!',
reply_to=msg1
)
print_success(f"Создан ответ на сообщение #{msg3.id}")
reply_notif = Notification.objects.filter(
recipient=user1,
notification_type__code='chat_reply'
).first()
if reply_notif:
print_success(f"[+] Уведомление об ответе: {reply_notif.title}")
else:
print_error("Уведомление об ответе НЕ создано!")
total = Notification.objects.filter(recipient__in=[user1, user2]).count()
print_info(f"Всего создано уведомлений: {total}")
def test_requests(user1, user2):
"""Тестирует signals модуля Requests."""
print_header("ТЕСТ 2: Requests")
Notification.objects.filter(recipient__in=[user1, user2]).delete()
# 1. Создаем заявку
request = Request.objects.create(
employee=user1,
type='vacation',
title='Отпуск',
comment='Прошу предоставить отпуск',
status='pending',
approver=user2
)
print_success(f"Создана заявка #{request.id}")
new_request_notif = Notification.objects.filter(
notification_type__code='request_new'
).first()
if new_request_notif:
print_success(f"✓ Уведомление о новой заявке: {new_request_notif.title}")
else:
print_error("Уведомление о новой заявке НЕ создано!")
# 2. Одобряем заявку
from django.utils import timezone
request.status = 'approved'
request.decided_at = timezone.now()
request.save()
print_success(f"Заявка #{request.id} одобрена")
approved_notif = Notification.objects.filter(
recipient=user1,
notification_type__code='request_approved'
).first()
if approved_notif:
print_success(f"✓ Уведомление об одобрении: {approved_notif.title}")
else:
print_error("Уведомление об одобрении НЕ создано!")
# 3. Добавляем комментарий
comment = RequestComment.objects.create(
request=request,
author=user2,
text='Одобрено, хорошего отдыха!'
)
print_success(f"Добавлен комментарий #{comment.id}")
comment_notif = Notification.objects.filter(
recipient=user1,
notification_type__code='request_comment'
).first()
if comment_notif:
print_success(f"✓ Уведомление о комментарии: {comment_notif.title}")
else:
print_error("Уведомление о комментарии НЕ создано!")
total = Notification.objects.filter(recipient__in=[user1, user2]).count()
print_info(f"Всего создано уведомлений: {total}")
def test_documents(user1, user2):
"""Тестирует signals модуля Documents."""
print_header("ТЕСТ 3: Documents")
Notification.objects.filter(recipient__in=[user1, user2]).delete()
# 1. Создаем документ для конкретных получателей
doc = Document.objects.create(
title='Важный документ',
description='Требует ознакомления',
uploaded_by=user1
)
doc.recipients.add(user2)
print_success(f"Создан документ #{doc.id}")
doc_notif = Notification.objects.filter(
recipient=user2,
notification_type__code='document_ready'
).first()
if doc_notif:
print_success(f"✓ Уведомление о документе: {doc_notif.title}")
else:
print_error("Уведомление о документе НЕ создано!")
# 2. Ознакомление с документом
ack = DocumentAcknowledgement.objects.create(
document=doc,
user=user2
)
print_success(f"Пользователь {user2.username} ознакомился")
# Проверяем уведомление о полном ознакомлении
all_ack_notif = Notification.objects.filter(
recipient=user1,
notification_type__code='document_signed_all'
).first()
if all_ack_notif:
print_success(f"✓ Уведомление о полном ознакомлении: {all_ack_notif.title}")
else:
print_error("Уведомление о полном ознакомлении НЕ создано!")
total = Notification.objects.filter(recipient__in=[user1, user2]).count()
print_info(f"Всего создано уведомлений: {total}")
def test_calendar(user1, user2):
"""Тестирует signals модуля Calendar."""
print_header("ТЕСТ 4: Calendar")
Notification.objects.filter(recipient__in=[user1, user2]).delete()
from datetime import date, timedelta
# 1. Создаем событие компании
event = CalendarEvent.objects.create(
title='Общее собрание',
description='Ежеквартальное собрание',
start_date=date.today() + timedelta(days=7),
created_by=user1
)
print_success(f"Создано событие #{event.id}")
event_notif = Notification.objects.filter(
notification_type__code='event_created'
).first()
if event_notif:
print_success(f"✓ Уведомление о событии: {event_notif.title}")
print_info(f" Получателей: {Notification.objects.filter(notification_type__code='event_created').count()}")
else:
print_error("Уведомление о событии НЕ создано!")
# 2. Изменяем событие
Notification.objects.all().delete() # Очищаем для чистоты теста
event.title = 'ВАЖНОЕ Общее собрание'
event.save()
print_success(f"Событие #{event.id} изменено")
changed_notif = Notification.objects.filter(
notification_type__code='event_changed'
).first()
if changed_notif:
print_success(f"✓ Уведомление об изменении: {changed_notif.title}")
else:
print_error("Уведомление об изменении НЕ создано!")
# 3. Удаляем событие
Notification.objects.all().delete()
event.delete()
print_success("Событие удалено")
cancelled_notif = Notification.objects.filter(
notification_type__code='event_cancelled'
).first()
if cancelled_notif:
print_success(f"✓ Уведомление об отмене: {cancelled_notif.title}")
else:
print_error("Уведомление об отмене НЕ создано!")
def test_feed(user1, user2):
"""Тестирует signals модуля Feed."""
print_header("ТЕСТ 5: Feed")
Notification.objects.filter(recipient__in=[user1, user2]).delete()
# 1. Создаем публикацию компании
post = Post.objects.create(
author=user1,
title='Важная новость',
body='Текст важной новости для всех сотрудников',
type='company'
)
print_success(f"Создана публикация #{post.id}")
post_notif = Notification.objects.filter(
notification_type__code='feed_new_post'
).first()
if post_notif:
print_success(f"✓ Уведомление о публикации: {post_notif.title}")
print_info(f" Получателей: {Notification.objects.filter(notification_type__code='feed_new_post').count()}")
else:
print_error("Уведомление о публикации НЕ создано!")
# 2. Добавляем комментарий
comment = Comment.objects.create(
post=post,
author=user2,
text='Отличная новость!'
)
print_success(f"Добавлен комментарий #{comment.id}")
comment_notif = Notification.objects.filter(
recipient=user1,
notification_type__code='feed_post_comment'
).first()
if comment_notif:
print_success(f"✓ Уведомление о комментарии: {comment_notif.title}")
else:
print_error("Уведомление о комментарии НЕ создано!")
# 3. Тестируем функцию реакции
from feed.notification_signals import notify_post_reaction
notify_post_reaction(post, user2)
print_success("Вызвана функция реакции")
reaction_notif = Notification.objects.filter(
recipient=user1,
notification_type__code='feed_post_reaction'
).first()
if reaction_notif:
print_success(f"✓ Уведомление о реакции: {reaction_notif.title}")
else:
print_error("Уведомление о реакции НЕ создано!")
def main():
"""Основная функция запуска всех тестов."""
print(f"{Colors.BOLD}{Colors.OKBLUE}")
print("="*60)
print(" ТЕСТИРОВАНИЕ СИСТЕМЫ УВЕДОМЛЕНИЙ - ЭТАП 3")
print(" Интеграция с модулями через signals")
print("="*60)
print(f"{Colors.ENDC}")
try:
# Подготовка
user1, user2 = get_or_create_test_users()
# Запуск тестов
test_communications(user1, user2)
test_requests(user1, user2)
test_documents(user1, user2)
test_calendar(user1, user2)
test_feed(user1, user2)
# Итоговая статистика
print_header("ИТОГИ")
total_notifications = Notification.objects.count()
total_types_used = Notification.objects.values(
'notification_type__code'
).distinct().count()
print_success(f"Всего уведомлений в БД: {total_notifications}")
print_success(f"Использовано типов: {total_types_used}")
print(f"\n{Colors.OKGREEN}{Colors.BOLD}✓ ВСЕ ТЕСТЫ ЗАВЕРШЕНЫ!{Colors.ENDC}\n")
except Exception as e:
print_error(f"ОШИБКА: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
main()

View File

@ -1,106 +0,0 @@
#!/usr/bin/env python
"""
Скрипт для тестирования пагинации в API эндпоинтах.
Запуск: python backend/test_pagination.py
"""
import os
import sys
import django
# Настройка Django
sys.path.insert(0, os.path.dirname(__file__))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
django.setup()
from django.contrib.auth import get_user_model
from api.client import get_api_client
from django.test import RequestFactory
User = get_user_model()
def test_pagination():
"""Тестирует пагинацию для всех основных эндпоинтов."""
# Создаём fake request для get_api_client
factory = RequestFactory()
# Берём первого суперпользователя
admin = User.objects.filter(is_superuser=True).first()
if not admin:
print("❌ Нет суперпользователя в БД")
return
print(f"✅ Используем пользователя: {admin.email}\n")
# Создаём fake HTTP request
fake_request = factory.get('/')
fake_request.user = admin
# Получаем API client
api = get_api_client(fake_request)
endpoints = [
('Отделы', 'v1/departments/'),
('Сотрудники', 'v1/employees/'),
('Документы', 'v1/documents/'),
('Заявления', 'v1/requests/'),
('Новости', 'v1/posts/'),
]
print("=" * 70)
print("ПРОВЕРКА ПАГИНАЦИИ API")
print("=" * 70 + "\n")
for name, endpoint in endpoints:
print(f"📍 {name} ({endpoint})")
print("-" * 70)
# Запрос без параметров
resp = api.get(endpoint)
if not resp.ok:
print(f" ❌ Ошибка: HTTP {resp.status}")
print()
continue
data = resp.json
# Проверяем формат ответа
if isinstance(data, dict):
if 'results' in data:
print(f" ✅ Пагинация ВКЛЮЧЕНА")
print(f" 📊 Всего: {data.get('count', '?')}")
print(f" 📄 На странице: {len(data.get('results', []))}")
print(f" ⏭️ Следующая: {'Да' if data.get('next') else 'Нет'}")
print(f" ⏮️ Предыдущая: {'Да' if data.get('previous') else 'Нет'}")
else:
print(f" ⚠️ Словарь БЕЗ 'results' (возможно другой формат)")
print(f" 📄 Ключи: {list(data.keys())[:5]}")
elif isinstance(data, list):
print(f" ❌ Пагинация ОТКЛЮЧЕНА (возвращается список)")
print(f" 📄 Элементов: {len(data)}")
else:
print(f" ⚠️ Неожиданный тип: {type(data)}")
# Тест с параметром page=2
print("\n 🔍 Тест page=2:")
resp2 = api.get(endpoint, params={'page': 2})
if resp2.ok:
data2 = resp2.json
if isinstance(data2, dict) and 'results' in data2:
print(f" ✅ Страница 2 работает ({len(data2.get('results', []))} элементов)")
else:
print(f" ⚠️ Страница 2: неожиданный формат")
else:
print(f" ⚠️ Страница 2: HTTP {resp2.status}")
print()
print("=" * 70)
print("ПРОВЕРКА ЗАВЕРШЕНА")
print("=" * 70)
if __name__ == '__main__':
test_pagination()

View File

@ -1,310 +0,0 @@
#!/usr/bin/env python
"""
Скрипт для тестирования функциональности получателей в заявлениях
"""
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
django.setup()
from django.contrib.auth import get_user_model
from requests_app.models import Request
from employees.models import Department, EmployeeDepartment
from requests_app.enums import RequestStatus, RequestType
User = get_user_model()
def print_section(title):
print(f"\n{'='*60}")
print(f" {title}")
print(f"{'='*60}\n")
def test_model_fields():
"""Тест 1: Проверка новых полей модели"""
print_section("ТЕСТ 1: Проверка полей модели Request")
# Проверяем, что поля существуют
request = Request.objects.first()
if request:
print(f"✓ Найдена заявка: #{request.id} - {request.display_title}")
print(f" - departments (ManyToMany): {request.departments.count()} отделов")
print(f" - recipients (ManyToMany): {request.recipients.count()} получателей")
print(f" - cc_users (ManyToMany): {request.cc_users.count()} в копии")
print(f" - sent_to_all_department: {request.sent_to_all_department}")
print(f" - department (старое поле): {request.department}")
else:
print("⚠ Нет заявок в БД. Создадим тестовую...")
# Создаем тестовые данные
author = User.objects.filter(is_active=True).first()
if not author:
print("✗ Не найдено активных пользователей!")
return False
request = Request.objects.create(
employee=author,
type=RequestType.VACATION,
title="Тестовая заявка с получателями",
date_from="2025-12-10",
date_to="2025-12-20",
comment="Тест новой функциональности",
status=RequestStatus.PENDING
)
print(f"✓ Создана заявка #{request.id}")
return True
def test_add_recipients():
"""Тест 2: Добавление получателей"""
print_section("ТЕСТ 2: Добавление получателей")
# Находим или создаем заявку
request = Request.objects.first()
if not request:
print("✗ Нет заявок для тестирования")
return False
print(f"Работаем с заявкой #{request.id}")
# Получаем пользователей для теста
users = User.objects.filter(is_active=True).exclude(
id=request.employee_id
)[:3]
if len(users) < 2:
print("⚠ Недостаточно пользователей для теста")
return False
# Добавляем основных получателей
recipient1, recipient2 = users[0], users[1]
request.add_recipient(recipient1, is_cc=False)
request.add_recipient(recipient2, is_cc=False)
print(f"✓ Добавлены основные получатели:")
print(f" - {recipient1.get_full_name() or recipient1.username}")
print(f" - {recipient2.get_full_name() or recipient2.username}")
# Добавляем в копию
if len(users) >= 3:
cc_user = users[2]
request.add_recipient(cc_user, is_cc=True)
print(f"✓ Добавлен в копию:")
print(f" - {cc_user.get_full_name() or cc_user.username}")
# Проверяем количество
print(f"\nИтого:")
print(f" - Основных получателей: {request.recipients.count()}")
print(f" - В копии: {request.cc_users.count()}")
return True
def test_is_recipient():
"""Тест 3: Проверка метода is_recipient"""
print_section("ТЕСТ 3: Метод is_recipient()")
request = Request.objects.first()
if not request:
print("✗ Нет заявок для тестирования")
return False
print(f"Заявка #{request.id}")
# Проверяем автора
print(f"\n1. Автор заявки:")
print(f" {request.employee.get_full_name() or request.employee.username}")
print(f" is_recipient: {request.is_recipient(request.employee)}")
# Проверяем получателей
if request.recipients.exists():
recipient = request.recipients.first()
print(f"\n2. Основной получатель:")
print(f" {recipient.get_full_name() or recipient.username}")
print(f" is_recipient: ✓ {request.is_recipient(recipient)}")
# Проверяем CC
if request.cc_users.exists():
cc_user = request.cc_users.first()
print(f"\n3. Пользователь в копии:")
print(f" {cc_user.get_full_name() or cc_user.username}")
print(f" is_recipient: ✓ {request.is_recipient(cc_user)}")
# Проверяем случайного пользователя
random_user = User.objects.exclude(
id__in=[request.employee_id] +
list(request.recipients.values_list('id', flat=True)) +
list(request.cc_users.values_list('id', flat=True))
).first()
if random_user:
print(f"\n4. Случайный пользователь:")
print(f" {random_user.get_full_name() or random_user.username}")
print(f" is_recipient: {request.is_recipient(random_user)}")
return True
def test_departments():
"""Тест 4: Работа с несколькими отделами"""
print_section("ТЕСТ 4: Множественные отделы")
request = Request.objects.first()
if not request:
print("✗ Нет заявок для тестирования")
return False
# Получаем отделы
departments = Department.objects.all()[:2]
if len(departments) < 2:
print("⚠ Недостаточно отделов для теста")
if len(departments) == 1:
request.departments.add(departments[0])
print(f"✓ Добавлен 1 отдел: {departments[0].name}")
else:
request.departments.set(departments)
print(f"✓ Добавлено {len(departments)} отделов:")
for dept in departments:
print(f" - {dept.name}")
print(f"\nВсего отделов: {request.departments.count()}")
return True
def test_sent_to_all():
"""Тест 5: Флаг sent_to_all_department"""
print_section("ТЕСТ 5: Флаг sent_to_all_department")
request = Request.objects.first()
if not request:
print("✗ Нет заявок для тестирования")
return False
# Включаем флаг
request.sent_to_all_department = True
request.save()
print(f"✓ Флаг sent_to_all_department установлен в True")
# Проверяем, что есть отделы
dept_count = request.departments.count()
print(f" Выбрано отделов: {dept_count}")
if dept_count > 0:
# Считаем сотрудников в этих отделах
dept_employees = User.objects.filter(
departments_links__department__in=request.departments.all(),
departments_links__is_active=True,
is_active=True
).distinct().count()
print(f" Сотрудников в отделах: {dept_employees}")
print(f" Они все теперь видят эту заявку!")
return True
def test_all_recipients_property():
"""Тест 6: Свойство all_recipients"""
print_section("ТЕСТ 6: Свойство all_recipients")
request = Request.objects.first()
if not request:
print("✗ Нет заявок для тестирования")
return False
all_recip = request.all_recipients
primary = request.primary_recipients
print(f"Всего получателей (recipients + cc): {all_recip.count()}")
print(f"Основных получателей: {primary.count()}")
print(f"В копии: {request.cc_users.count()}")
if all_recip.exists():
print("\nСписок всех получателей:")
for user in all_recip:
is_primary = request.recipients.filter(id=user.id).exists()
is_cc = request.cc_users.filter(id=user.id).exists()
role = "основной" if is_primary else "CC"
print(f" - {user.get_full_name() or user.username} ({role})")
return True
def test_api_serializer():
"""Тест 7: Проверка сериализатора"""
print_section("ТЕСТ 7: Сериализация через API")
from api.v1.requests_app.serializers import RequestReadSerializer
from rest_framework.request import Request as DRFRequest
from django.test import RequestFactory
request_obj = Request.objects.first()
if not request_obj:
print("✗ Нет заявок для тестирования")
return False
# Создаем mock request
factory = RequestFactory()
django_request = factory.get('/')
django_request.user = request_obj.employee
drf_request = DRFRequest(django_request)
# Сериализуем
serializer = RequestReadSerializer(
request_obj,
context={'request': drf_request}
)
data = serializer.data
print(f"Заявка #{data['id']}:")
print(f" - departments: {len(data.get('departments', []))}")
print(f" - recipients: {len(data.get('recipients', []))}")
print(f" - cc_users: {len(data.get('cc_users', []))}")
print(f" - recipient_count: {data.get('recipient_count', 0)}")
print(f" - cc_count: {data.get('cc_count', 0)}")
print(f" - is_recipient: {data.get('is_recipient', False)}")
print(f" - sent_to_all_department: {data.get('sent_to_all_department', False)}")
return True
def run_all_tests():
"""Запуск всех тестов"""
print("\n" + "="*60)
print(" 🧪 ТЕСТИРОВАНИЕ ФУНКЦИОНАЛЬНОСТИ ПОЛУЧАТЕЛЕЙ")
print("="*60)
tests = [
("Проверка полей модели", test_model_fields),
("Добавление получателей", test_add_recipients),
("Метод is_recipient", test_is_recipient),
("Множественные отделы", test_departments),
("Флаг sent_to_all_department", test_sent_to_all),
("Свойство all_recipients", test_all_recipients_property),
("Сериализация API", test_api_serializer),
]
results = []
for name, test_func in tests:
try:
result = test_func()
results.append((name, result))
except Exception as e:
print(f"\n✗ ОШИБКА в тесте '{name}':")
print(f" {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
results.append((name, False))
# Итоги
print_section("ИТОГИ ТЕСТИРОВАНИЯ")
passed = sum(1 for _, r in results if r)
total = len(results)
for name, result in results:
status = "✓ PASSED" if result else "✗ FAILED"
print(f"{status:12} | {name}")
print(f"\n{'='*60}")
print(f"Пройдено: {passed}/{total} тестов ({passed*100//total}%)")
print(f"{'='*60}\n")
return passed == total
if __name__ == '__main__':
success = run_all_tests()
exit(0 if success else 1)

View File

@ -1,247 +0,0 @@
#!/usr/bin/env python
"""
Тестирование API получателей заявлений
"""
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
django.setup()
import json
from django.contrib.auth import get_user_model
from django.test import RequestFactory
from rest_framework.test import force_authenticate
from api.v1.requests_app.views import RequestViewSet
from api.v1.requests_app.serializers import RequestWriteSerializer
from requests_app.models import Request
from employees.models import Department
User = get_user_model()
def test_create_request_with_recipients():
"""Тест создания заявки с получателями через API"""
print("\n" + "="*60)
print(" ТЕСТ: Создание заявки с получателями")
print("="*60)
# Получаем пользователей
author = User.objects.filter(is_active=True).first()
users = User.objects.filter(is_active=True).exclude(id=author.id)[:3]
dept = Department.objects.first()
if not author or len(users) < 2 or not dept:
print("✗ Недостаточно данных для теста")
return False
recipient1, recipient2 = users[0], users[1]
cc_user = users[2] if len(users) > 2 else None
# Данные для создания
data = {
'type': 'VACATION',
'title': 'Отпуск с API тестом получателей',
'date_from': '2025-12-10',
'date_to': '2025-12-20',
'comment': 'Тест API',
'department_ids': [dept.id],
'recipient_ids': [recipient1.id, recipient2.id],
'sent_to_all_department': False
}
if cc_user:
data['cc_user_ids'] = [cc_user.id]
print(f"\nДанные запроса:")
print(f" - author: {author.get_full_name() or author.username}")
print(f" - recipients: {recipient1.get_full_name()}, {recipient2.get_full_name()}")
if cc_user:
print(f" - cc: {cc_user.get_full_name()}")
print(f" - department: {dept.name}")
# Создаем mock request
factory = RequestFactory()
request = factory.post('/api/v1/requests/', data, format='json')
force_authenticate(request, user=author)
# Сериализация
serializer = RequestWriteSerializer(
data=data,
context={'request': request}
)
if not serializer.is_valid():
print(f"\n✗ Ошибки валидации: {serializer.errors}")
return False
# Создание объекта
request_obj = serializer.save(employee=author)
print(f"\n✓ Заявка создана: #{request_obj.id}")
print(f" - recipients: {request_obj.recipients.count()}")
print(f" - cc_users: {request_obj.cc_users.count()}")
print(f" - departments: {request_obj.departments.count()}")
# Проверяем правильность
assert request_obj.recipients.count() == 2
assert request_obj.is_recipient(recipient1)
assert request_obj.is_recipient(recipient2)
if cc_user:
assert request_obj.cc_users.count() == 1
assert request_obj.is_recipient(cc_user)
print("\n✓ Все проверки прошли успешно!")
return True
def test_queryset_addressed_to_me():
"""Тест фильтрации ?addressed_to_me=true"""
print("\n" + "="*60)
print(" ТЕСТ: Фильтр ?addressed_to_me=true")
print("="*60)
# Находим заявку с получателями
request_obj = Request.objects.filter(
recipients__isnull=False
).distinct().first()
if not request_obj:
print("⚠ Нет заявок с получателями")
return False
recipient = request_obj.recipients.first()
if not recipient:
print("⚠ Нет получателей")
return False
print(f"\nЗаявка: #{request_obj.id}")
print(f"Получатель: {recipient.get_full_name() or recipient.username}")
# Mock request
factory = RequestFactory()
django_request = factory.get('/api/v1/requests/?addressed_to_me=true')
force_authenticate(django_request, user=recipient)
# ViewSet
viewset = RequestViewSet()
viewset.request = django_request
viewset.action = 'list'
viewset.format_kwarg = None
# Получаем queryset
qs = viewset.get_queryset()
print(f"\nКоличество заявок для пользователя: {qs.count()}")
# Проверяем, что наша заявка в выборке
if qs.filter(id=request_obj.id).exists():
print("✓ Заявка найдена в выборке для получателя")
return True
else:
print("✗ Заявка НЕ найдена в выборке")
return False
def test_update_recipients():
"""Тест обновления получателей"""
print("\n" + "="*60)
print(" ТЕСТ: Обновление получателей")
print("="*60)
request_obj = Request.objects.first()
if not request_obj:
print("✗ Нет заявок")
return False
users = User.objects.filter(is_active=True).exclude(
id=request_obj.employee_id
)[:4]
if len(users) < 3:
print("⚠ Недостаточно пользователей")
return False
print(f"\nЗаявка: #{request_obj.id}")
print(f"Было получателей: {request_obj.recipients.count()}")
print(f"Было CC: {request_obj.cc_users.count()}")
# Новые данные
new_recipients = [users[0].id, users[1].id, users[2].id]
new_cc = [users[3].id] if len(users) > 3 else []
data = {
'recipient_ids': new_recipients,
'cc_user_ids': new_cc,
}
# Mock request
factory = RequestFactory()
django_request = factory.patch(
f'/api/v1/requests/{request_obj.id}/',
data,
format='json'
)
force_authenticate(django_request, user=request_obj.employee)
# Сериализация
serializer = RequestWriteSerializer(
request_obj,
data=data,
partial=True,
context={'request': django_request}
)
if not serializer.is_valid():
print(f"\n✗ Ошибки: {serializer.errors}")
return False
updated_obj = serializer.save()
print(f"\n✓ Обновлено:")
print(f" - recipients: {updated_obj.recipients.count()}")
print(f" - cc_users: {updated_obj.cc_users.count()}")
assert updated_obj.recipients.count() == 3
if new_cc:
assert updated_obj.cc_users.count() == 1
print("✓ Все проверки прошли!")
return True
if __name__ == '__main__':
print("\n" + "="*60)
print(" 🧪 ТЕСТИРОВАНИЕ API ПОЛУЧАТЕЛЕЙ")
print("="*60)
tests = [
("Создание с получателями", test_create_request_with_recipients),
("Фильтр addressed_to_me", test_queryset_addressed_to_me),
("Обновление получателей", test_update_recipients),
]
results = []
for name, test_func in tests:
try:
result = test_func()
results.append((name, result))
except Exception as e:
print(f"\n✗ ОШИБКА: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
results.append((name, False))
# Итоги
print("\n" + "="*60)
print(" ИТОГИ")
print("="*60)
for name, result in results:
status = "✓ PASSED" if result else "✗ FAILED"
print(f"{status:12} | {name}")
passed = sum(1 for _, r in results if r)
total = len(results)
print(f"\nПройдено: {passed}/{total}")

View File

@ -1,148 +0,0 @@
#!/usr/bin/env python
"""
Тестовый скрипт для проверки API регистрации.
Отправляет запросы на /api/v1/auth/register/ и показывает ответы.
"""
import json
import sys
import os
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.test import Client
from django.urls import reverse
def test_register():
"""Тестируем регистрацию с валидными данными."""
client = Client()
url = reverse("api:v1:register")
# Тестовые данные
payload = {
"first_name": "Тест",
"last_name": "Тестов",
"phone_number": "+79991234567",
"email": "test@example.com",
"birth_date": "1990-01-01",
"password": "password123",
"telegram": "@test_user",
"whatsapp": "",
"wechat": "",
}
print(f"Отправка запроса на: {url}")
print(f"Данные: {json.dumps(payload, indent=2, ensure_ascii=False)}")
print("-" * 60)
response = client.post(
url,
data=json.dumps(payload),
content_type="application/json"
)
print(f"Статус: {response.status_code}")
print(f"Content-Type: {response.get('Content-Type')}")
try:
data = response.json()
print(f"Ответ: {json.dumps(data, indent=2, ensure_ascii=False)}")
except Exception as e:
print(f"Не удалось распарсить JSON: {e}")
print(f"Тело ответа: {response.content.decode('utf-8')}")
return response
def test_register_missing_contact():
"""Тестируем регистрацию без контактов."""
client = Client()
url = reverse("api:v1:register")
payload = {
"first_name": "Тест",
"last_name": "Тестов",
"phone_number": "+79991234567",
"email": "test2@example.com",
"birth_date": "1990-01-01",
"password": "password123",
"telegram": "",
"whatsapp": "",
"wechat": "",
}
print("\n" + "=" * 60)
print("Тест без контактов:")
print(f"Данные: {json.dumps(payload, indent=2, ensure_ascii=False)}")
print("-" * 60)
response = client.post(
url,
data=json.dumps(payload),
content_type="application/json"
)
print(f"Статус: {response.status_code}")
try:
data = response.json()
print(f"Ответ: {json.dumps(data, indent=2, ensure_ascii=False)}")
except Exception as e:
print(f"Не удалось распарсить JSON: {e}")
return response
def test_register_invalid_phone():
"""Тестируем регистрацию с неправильным телефоном."""
client = Client()
url = reverse("api:v1:register")
payload = {
"first_name": "Тест",
"last_name": "Тестов",
"phone_number": "123", # неправильный формат
"email": "test3@example.com",
"birth_date": "1990-01-01",
"password": "password123",
"telegram": "@test_user",
"whatsapp": "",
"wechat": "",
}
print("\n" + "=" * 60)
print("Тест с неправильным телефоном:")
print(f"Данные: {json.dumps(payload, indent=2, ensure_ascii=False)}")
print("-" * 60)
response = client.post(
url,
data=json.dumps(payload),
content_type="application/json"
)
print(f"Статус: {response.status_code}")
try:
data = response.json()
print(f"Ответ: {json.dumps(data, indent=2, ensure_ascii=False)}")
except Exception as e:
print(f"Не удалось распарсить JSON: {e}")
return response
if __name__ == "__main__":
print("=" * 60)
print("ТЕСТ API РЕГИСТРАЦИИ")
print("=" * 60)
test_register()
test_register_missing_contact()
test_register_invalid_phone()
print("\n" + "=" * 60)
print("Тесты завершены")
print("=" * 60)

View File

@ -1,93 +0,0 @@
"""
Тест: обновление сообщений при редактировании сообщения, на которое ответили
"""
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'eusrr_backend.settings')
django.setup()
from communications.models import Message, Chat
from employees.models import Employee
from django.contrib.auth import get_user_model
User = get_user_model()
def test_reply_update():
"""Проверяем, что при редактировании сообщения обновляются все ответы на него"""
print("🧪 Тест: обновление reply_to при редактировании")
print("=" * 60)
# Находим тестовый чат с сообщениями
chat = Chat.objects.filter(
type='private'
).prefetch_related('messages').first()
if not chat:
print("❌ Не найдено приватных чатов")
return
print(f"✓ Чат: {chat}")
# Находим сообщение, на которое есть ответы
original_msg = Message.objects.filter(
chat=chat,
replies__isnull=False # Есть ответы на это сообщение
).first()
if not original_msg:
print("❌ Не найдено сообщение с ответами")
print("Создадим тестовые сообщения...")
# Создаём тестовые сообщения
user1 = chat.members.first()
user2 = chat.members.last()
if user1 == user2 or not user2:
print("❌ Нужно минимум 2 участника")
return
# Сообщение 1 (на которое ответят)
original_msg = Message.objects.create(
chat=chat,
author=user1,
content="Оригинальное сообщение для редактирования"
)
print(f"✓ Создано оригинальное сообщение: ID={original_msg.id}")
# Сообщение 2 (ответ на первое)
reply_msg = Message.objects.create(
chat=chat,
author=user2,
content="Ответ на оригинальное сообщение",
reply_to=original_msg
)
print(f"✓ Создан ответ: ID={reply_msg.id}")
# Находим все ответы на это сообщение
replies = Message.objects.filter(
reply_to=original_msg,
chat=chat
)
print(f"\n📝 Оригинальное сообщение (ID={original_msg.id}):")
print(f" Контент: '{original_msg.content}'")
print(f" Ответов на него: {replies.count()}")
for reply in replies:
print(f"\n📨 Ответ (ID={reply.id}):")
print(f" Контент: '{reply.content}'")
print(f" reply_to.id: {reply.reply_to.id}")
print(f" reply_to.content: '{reply.reply_to.content}'")
print("\n" + "=" * 60)
print("✅ Структура готова для теста!")
print("\nТеперь:")
print(f"1. Откройте чат ID={chat.id}")
print(f"2. Отредактируйте сообщение ID={original_msg.id}")
print(f"3. Проверьте, что сообщения с ID={[r.id for r in replies]} обновились")
print("4. В reply-preview должен быть новый текст")
if __name__ == '__main__':
test_reply_update()

View File

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

View File

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

View File

@ -1,279 +0,0 @@
"""
Тесты middleware для проверки верификации email.
Проверяют что неверифицированные пользователи:
1. Могут получить доступ только к определённым страницам
2. Редиректятся на страницу верификации
3. Могут верифицировать свой email
"""
import pytest
from django.contrib.auth import get_user_model
from django.test import Client, RequestFactory
from django.urls import reverse
from eusrr_backend.middleware import EmailVerificationMiddleware
Employee = get_user_model()
def make_user(email, verified=True, **kwargs):
"""Создаёт пользователя для тестов."""
phone = kwargs.get(
"phone_number", f"+7900{email[:7].replace('@', '').replace('.', '')}"
)
user = Employee.objects.create_user(
email=email,
password="TestPass123!",
first_name=kwargs.get("first_name", "Test"),
last_name=kwargs.get("last_name", "User"),
phone_number=phone,
telegram=kwargs.get("telegram", f"@{email.split('@')[0]}"),
send_activation_email=False,
)
user.email_verified = verified
user.is_active = True
user.save(update_fields=["email_verified", "is_active"])
return user
@pytest.mark.django_db
class TestEmailVerificationMiddleware:
"""Тесты middleware проверки верификации email."""
def test_unverified_user_redirected_to_verification_page(self):
"""Неверифицированный пользователь редиректится на верификацию."""
client = Client()
user = make_user("unverified@test.com", verified=False)
# Логинимся
client.force_login(user)
# Пытаемся зайти на главную
response = client.get("/")
# Должен быть редирект на страницу верификации
assert response.status_code == 302
assert (
"verify-email" in response.url or "verify" in response.url.lower()
)
def test_verified_user_can_access_pages(self):
"""Верифицированный пользователь может работать нормально."""
client = Client()
user = make_user("verified@test.com", verified=True)
client.force_login(user)
# Можем зайти на главную (или получим 404, но не редирект)
response = client.get("/")
# Не должно быть редиректа на верификацию
if response.status_code == 302:
assert "verify" not in response.url.lower()
def test_unverified_user_can_access_verification_page(self):
"""Неверифицированный пользователь может зайти на верификацию."""
client = Client()
user = make_user("unverified@test.com", verified=False)
client.force_login(user)
# Заходим на страницу верификации
response = client.get(reverse("auth_front:verify_email"))
# Должны попасть на страницу (200 или 405, но не редирект)
assert response.status_code in [200, 405]
def test_unverified_user_can_access_resend_page(self):
"""Неверифицированный пользователь может запросить код повторно."""
client = Client()
user = make_user("unverified@test.com", verified=False)
client.force_login(user)
# Заходим на страницу повторной отправки
try:
response = client.get(reverse("auth_front:resend_email"))
# Должны попасть на страницу
assert response.status_code in [200, 405]
except Exception:
# Если нет такого урла - ничего страшного
pass
def test_unverified_user_can_access_api_endpoints(self):
"""Неверифицированный пользователь может работать с API."""
client = Client()
user = make_user("unverified@test.com", verified=False)
client.force_login(user)
# API должно работать (проверка прав на уровне ViewSet)
response = client.get("/api/v1/employees/me/")
# Не должно быть редиректа (либо 200, либо 403/401)
assert response.status_code in [200, 401, 403]
def test_anonymous_user_redirected_to_login(self):
"""Анонимный пользователь редиректится на логин."""
client = Client()
# Пытаемся зайти на главную без логина
response = client.get("/")
# Должен быть редирект на логин
assert response.status_code == 302
assert (
"login" in response.url.lower() or response.url == "/auth/login/"
)
def test_unverified_staff_can_access_admin(self):
"""Неверифицированный staff может зайти в админку."""
client = Client()
user = make_user("staff@test.com", verified=False)
user.is_staff = True
user.save()
client.force_login(user)
# Заходим в админку
response = client.get("/admin/")
# Должны попасть в админку (или редирект внутри админки)
assert response.status_code in [200, 302]
# Не должно быть редиректа на верификацию
if response.status_code == 302:
assert "verify" not in response.url.lower()
def test_middleware_allows_static_and_media(self):
"""Middleware пропускает статику и медиа."""
factory = RequestFactory()
# Создаём middleware
def dummy_view(request):
from django.http import HttpResponse
return HttpResponse("OK")
middleware = EmailVerificationMiddleware(dummy_view)
# Тестируем статику
request = factory.get("/static/css/style.css")
request.user = make_user("unverified@test.com", verified=False)
response = middleware(request)
# Должен пропустить (вызвать view)
assert response.status_code == 200
assert response.content == b"OK"
# Тестируем медиа
request = factory.get("/media/avatars/user.jpg")
request.user = make_user("unverified2@test.com", verified=False)
response = middleware(request)
# Должен пропустить
assert response.status_code == 200
@pytest.mark.django_db
class TestVerificationPageAccess:
"""Тесты доступа к страницам верификации."""
def test_verification_page_exists(self):
"""Страница верификации существует."""
client = Client()
try:
url = reverse("auth_front:verify_email")
response = client.get(url)
# Страница должна существовать
assert response.status_code in [200, 302, 405]
except Exception:
# Если нет урла - тест пропускаем
pytest.skip("Verification page URL not configured")
def test_resend_page_exists(self):
"""Страница повторной отправки существует."""
client = Client()
try:
url = reverse("auth_front:resend_email")
response = client.get(url)
# Страница должна существовать
assert response.status_code in [200, 302, 405]
except Exception:
# Если нет урла - тест пропускаем
pytest.skip("Resend page URL not configured")
def test_verified_user_redirected_from_verification_page(self):
"""Верифицированный пользователь редиректится с верификации."""
client = Client()
user = make_user("verified@test.com", verified=True)
client.force_login(user)
try:
url = reverse("auth_front:verify_email")
response = client.get(url)
# Может быть редирект или просто сообщение
# Главное - не ошибка
assert response.status_code in [200, 302]
except Exception:
pytest.skip("Verification page URL not configured")
@pytest.mark.django_db
class TestCriticalOperationsRestrictions:
"""
Тесты что критичные операции заблокированы
для неверифицированных пользователей.
"""
def test_unverified_cannot_create_department_via_web(self):
"""Неверифицированный не может создать отдел через веб."""
client = Client()
user = make_user("staff@test.com", verified=False)
user.is_staff = True
user.save()
client.force_login(user)
# Пытаемся создать отдел
response = client.post(
"/api/v1/departments/",
{"name": "Test Department"},
content_type="application/json",
)
# Должен быть запрещён (или редирект на верификацию)
assert response.status_code in [302, 403]
if response.status_code == 302:
assert "verify" in response.url.lower()
def test_unverified_cannot_modify_other_users(self):
"""Неверифицированный staff не может менять других."""
client = Client()
staff = make_user("staff@test.com", verified=False)
staff.is_staff = True
staff.save()
other_user = make_user("other@test.com", verified=True)
client.force_login(staff)
# Пытаемся изменить другого пользователя
response = client.patch(
f"/api/v1/employees/{other_user.id}/",
{"first_name": "Hacked"},
content_type="application/json",
)
# Должен быть запрещён
assert response.status_code in [302, 403]

View File

@ -1 +0,0 @@
# Notifications tests package

View File

@ -1,274 +0,0 @@
"""
Тесты для API endpoints уведомлений
"""
from django.test import TestCase
from django.contrib.auth import get_user_model
from rest_framework.test import APIClient
from rest_framework import status
from notifications.models import (
NotificationCategory,
NotificationType,
Notification,
WebPushSubscription,
)
User = get_user_model()
def create_test_user(email='test@example.com', phone='+79990000001'):
"""Helper для создания тестового пользователя с обязательным phone_number"""
return User.objects.create_user(
email=email,
phone_number=phone,
password='testpass123',
first_name='Test',
last_name='User',
is_active=True
)
class NotificationAPITest(TestCase):
"""Тесты API уведомлений"""
def setUp(self):
self.client = APIClient()
self.user = create_test_user()
self.client.force_authenticate(user=self.user)
# Создаем категорию и тип
self.category = NotificationCategory.objects.create(
code='test_category',
name='Test Category'
)
self.notification_type = NotificationType.objects.create(
category=self.category,
code='test_type',
name='Test Type'
)
# Создаем тестовые уведомления
for i in range(5):
Notification.objects.create(
recipient=self.user,
notification_type=self.notification_type,
title=f'Notification {i+1}',
message=f'Message {i+1}',
short_message=f'Short {i+1}'
)
def test_get_notifications(self):
"""Получение списка уведомлений"""
response = self.client.get('/api/v1/notifications/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['total'], 5)
self.assertEqual(len(response.data['notifications']), 5)
def test_get_notifications_with_pagination(self):
"""Пагинация"""
response = self.client.get('/api/v1/notifications/?page=1&page_size=2')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['total'], 5)
self.assertEqual(len(response.data['notifications']), 2)
self.assertEqual(response.data['page'], 1)
def test_get_unread_notifications(self):
"""Фильтр непрочитанных"""
response = self.client.get('/api/v1/notifications/?unread_only=true')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['total'], 5) # Все непрочитанные
def test_get_unread_count(self):
"""Получение количества непрочитанных"""
response = self.client.get('/api/v1/notifications/count/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['count'], 5)
def test_mark_as_read(self):
"""Отметка уведомления как прочитанное"""
notification = Notification.objects.filter(recipient=self.user).first()
response = self.client.post(f'/api/v1/notifications/{notification.id}/read/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
notification.refresh_from_db()
self.assertTrue(notification.is_read)
def test_mark_all_as_read(self):
"""Отметка всех уведомлений как прочитанные"""
response = self.client.post('/api/v1/notifications/read-all/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
unread_count = Notification.objects.filter(
recipient=self.user,
is_read=False
).count()
self.assertEqual(unread_count, 0)
def test_delete_notification(self):
"""Удаление уведомления"""
notification = Notification.objects.filter(recipient=self.user).first()
response = self.client.delete(f'/api/v1/notifications/{notification.id}/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Проверяем что уведомление заархивировано, а не удалено
notification.refresh_from_db()
self.assertTrue(notification.is_archived)
def test_search_notifications(self):
"""Поиск уведомлений"""
response = self.client.get('/api/v1/notifications/?search=Notification 3')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['total'], 1)
self.assertEqual(response.data['notifications'][0]['title'], 'Notification 3')
def test_unauthorized_access(self):
"""Доступ без авторизации"""
self.client.force_authenticate(user=None)
response = self.client.get('/api/v1/notifications/')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
class WebPushAPITest(TestCase):
"""Тесты Web Push API"""
def setUp(self):
self.client = APIClient()
self.user = create_test_user()
self.client.force_authenticate(user=self.user)
def test_get_vapid_public_key(self):
"""Получение VAPID публичного ключа"""
response = self.client.get('/api/v1/notifications/push/vapid-key/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('vapid_public_key', response.data)
def test_subscribe_push(self):
"""Подписка на push-уведомления"""
data = {
'endpoint': 'https://fcm.googleapis.com/fcm/send/test123',
'keys': {
'p256dh': 'test_p256dh_key',
'auth': 'test_auth_key'
},
'device_name': 'Test Browser'
}
response = self.client.post('/api/v1/notifications/push/subscribe/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['status'], 'success')
# Проверяем что подписка создана
subscription = WebPushSubscription.objects.get(user=self.user)
self.assertEqual(subscription.endpoint, data['endpoint'])
self.assertEqual(subscription.device_name, 'Test Browser')
def test_subscribe_push_missing_fields(self):
"""Подписка с отсутствующими полями"""
data = {
'endpoint': 'https://fcm.googleapis.com/fcm/send/test123'
# Нет keys
}
response = self.client.post('/api/v1/notifications/push/subscribe/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_update_existing_subscription(self):
"""Обновление существующей подписки"""
# Создаем первую подписку
data1 = {
'endpoint': 'https://fcm.googleapis.com/fcm/send/test123',
'keys': {
'p256dh': 'old_key',
'auth': 'old_auth'
},
'device_name': 'Old Browser'
}
self.client.post('/api/v1/notifications/push/subscribe/', data1, format='json')
# Обновляем подписку с новыми ключами
data2 = {
'endpoint': 'https://fcm.googleapis.com/fcm/send/test123',
'keys': {
'p256dh': 'new_key',
'auth': 'new_auth'
},
'device_name': 'New Browser'
}
response = self.client.post('/api/v1/notifications/push/subscribe/', data2, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertFalse(response.data['created']) # Обновлена, не создана
# Проверяем обновление
subscription = WebPushSubscription.objects.get(user=self.user)
self.assertEqual(subscription.p256dh_key, 'new_key')
self.assertEqual(subscription.device_name, 'New Browser')
def test_unsubscribe_push(self):
"""Отписка от push-уведомлений"""
# Создаем подписку
subscription = WebPushSubscription.objects.create(
user=self.user,
endpoint='https://fcm.googleapis.com/fcm/send/test123',
p256dh_key='test_key',
auth_key='test_auth'
)
# Отписываемся
data = {'endpoint': subscription.endpoint}
response = self.client.delete('/api/v1/notifications/push/unsubscribe/', data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Проверяем что подписка удалена
self.assertFalse(
WebPushSubscription.objects.filter(id=subscription.id).exists()
)
def test_unsubscribe_all_push(self):
"""Отписка от всех подписок"""
# Создаем несколько подписок
for i in range(3):
WebPushSubscription.objects.create(
user=self.user,
endpoint=f'https://fcm.googleapis.com/fcm/send/test{i}',
p256dh_key=f'test_key_{i}',
auth_key=f'test_auth_{i}'
)
# Отписываемся от всех
response = self.client.delete('/api/v1/notifications/push/unsubscribe/', {}, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
# Проверяем что все подписки удалены
self.assertEqual(
WebPushSubscription.objects.filter(user=self.user).count(),
0
)
def test_get_subscriptions(self):
"""Получение списка подписок"""
# Создаем подписки
for i in range(3):
WebPushSubscription.objects.create(
user=self.user,
endpoint=f'https://fcm.googleapis.com/fcm/send/test{i}',
p256dh_key=f'test_key_{i}',
auth_key=f'test_auth_{i}',
device_name=f'Device {i}'
)
response = self.client.get('/api/v1/notifications/push/subscriptions/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data['subscriptions']), 3)

View File

@ -1,224 +0,0 @@
"""
Тесты для моделей уведомлений
"""
from django.test import TestCase
from django.contrib.auth import get_user_model
from notifications.models import (
NotificationCategory,
NotificationType,
Notification,
UserNotificationSettings,
WebPushSubscription,
)
User = get_user_model()
def create_test_user(email='test@example.com', phone='+79990000001'):
"""Helper для создания тестового пользователя с обязательным phone_number"""
return User.objects.create_user(
email=email,
phone_number=phone,
password='testpass123',
first_name='Test',
last_name='User',
is_active=True
)
class NotificationCategoryTest(TestCase):
"""Тесты категорий уведомлений"""
def setUp(self):
self.category = NotificationCategory.objects.create(
code='test_category',
name='Test Category',
icon='bi-test',
color='primary'
)
def test_category_creation(self):
"""Создание категории"""
self.assertEqual(self.category.code, 'test_category')
self.assertEqual(self.category.name, 'Test Category')
self.assertTrue(self.category.is_active)
def test_category_str(self):
"""Строковое представление"""
self.assertEqual(str(self.category), 'Test Category')
class NotificationTypeTest(TestCase):
"""Тесты типов уведомлений"""
def setUp(self):
self.category = NotificationCategory.objects.create(
code='test_category',
name='Test Category'
)
self.notification_type = NotificationType.objects.create(
category=self.category,
code='test_type',
name='Test Type',
default_enabled=True,
priority='normal'
)
def test_type_creation(self):
"""Создание типа уведомления"""
self.assertEqual(self.notification_type.code, 'test_type')
self.assertEqual(self.notification_type.category, self.category)
self.assertTrue(self.notification_type.default_enabled)
self.assertEqual(self.notification_type.priority, 'normal')
def test_type_str(self):
"""Строковое представление"""
self.assertEqual(str(self.notification_type), 'Test Category: Test Type')
class NotificationTest(TestCase):
"""Тесты уведомлений"""
def setUp(self):
self.user = create_test_user()
self.category = NotificationCategory.objects.create(
code='test_category',
name='Test Category'
)
self.notification_type = NotificationType.objects.create(
category=self.category,
code='test_type',
name='Test Type'
)
self.notification = Notification.objects.create(
recipient=self.user,
notification_type=self.notification_type,
title='Test Notification',
message='Test message',
short_message='Test'
)
def test_notification_creation(self):
"""Создание уведомления"""
self.assertEqual(self.notification.recipient, self.user)
self.assertEqual(self.notification.title, 'Test Notification')
self.assertFalse(self.notification.is_read)
self.assertFalse(self.notification.is_archived)
def test_mark_as_read(self):
"""Отметка как прочитанное"""
self.assertFalse(self.notification.is_read)
self.assertIsNone(self.notification.read_at)
self.notification.mark_as_read()
self.assertTrue(self.notification.is_read)
self.assertIsNotNone(self.notification.read_at)
def test_archive(self):
"""Архивирование"""
self.assertFalse(self.notification.is_archived)
self.assertIsNone(self.notification.archived_at)
self.notification.archive()
self.assertTrue(self.notification.is_archived)
self.assertIsNotNone(self.notification.archived_at)
class UserNotificationSettingsTest(TestCase):
"""Тесты настроек уведомлений пользователя"""
def setUp(self):
self.user = create_test_user()
self.category = NotificationCategory.objects.create(
code='test_category',
name='Test Category'
)
self.notification_type = NotificationType.objects.create(
category=self.category,
code='test_type',
name='Test Type'
)
self.settings = UserNotificationSettings.objects.create(
user=self.user,
notification_type=self.notification_type,
is_enabled=True,
send_web=True,
send_email=False
)
def test_settings_creation(self):
"""Создание настроек"""
self.assertEqual(self.settings.user, self.user)
self.assertTrue(self.settings.is_enabled)
self.assertTrue(self.settings.send_web)
self.assertFalse(self.settings.send_email)
class WebPushSubscriptionTest(TestCase):
"""Тесты Web Push подписок"""
def setUp(self):
self.user = create_test_user()
self.subscription = WebPushSubscription.objects.create(
user=self.user,
endpoint='https://fcm.googleapis.com/fcm/send/test123',
p256dh_key='test_p256dh_key',
auth_key='test_auth_key',
device_name='Test Device'
)
def test_subscription_creation(self):
"""Создание подписки"""
self.assertEqual(self.subscription.user, self.user)
self.assertEqual(self.subscription.device_name, 'Test Device')
self.assertTrue(self.subscription.is_active)
self.assertEqual(self.subscription.error_count, 0)
def test_mark_used(self):
"""Обновление времени использования"""
self.assertIsNone(self.subscription.last_used_at)
self.subscription.mark_used()
self.assertIsNotNone(self.subscription.last_used_at)
def test_increment_error(self):
"""Инкремент ошибок"""
self.assertEqual(self.subscription.error_count, 0)
self.subscription.increment_error('Test error')
self.assertEqual(self.subscription.error_count, 1)
self.assertEqual(self.subscription.last_error, 'Test error')
self.assertTrue(self.subscription.is_active)
def test_auto_deactivate_after_5_errors(self):
"""Автоматическая деактивация после 5 ошибок"""
for i in range(5):
self.subscription.increment_error(f'Error {i+1}')
self.subscription.refresh_from_db()
self.assertFalse(self.subscription.is_active)
self.assertEqual(self.subscription.error_count, 5)
def test_reset_errors(self):
"""Сброс ошибок"""
self.subscription.increment_error('Test error')
self.assertEqual(self.subscription.error_count, 1)
self.subscription.reset_errors()
self.assertEqual(self.subscription.error_count, 0)
self.assertEqual(self.subscription.last_error, '')
def test_unique_together(self):
"""Уникальность user + endpoint"""
with self.assertRaises(Exception):
WebPushSubscription.objects.create(
user=self.user,
endpoint='https://fcm.googleapis.com/fcm/send/test123',
p256dh_key='another_key',
auth_key='another_auth'
)

View File

@ -1,141 +0,0 @@
"""
Тесты для NotificationService
"""
from django.test import TestCase
from django.contrib.auth import get_user_model
from notifications.models import (
NotificationCategory,
NotificationType,
Notification,
UserNotificationSettings,
)
from notifications.services import NotificationService
User = get_user_model()
def create_test_user(email='test@example.com', phone='+79990000001'):
"""Helper для создания тестового пользователя с обязательным phone_number"""
return User.objects.create_user(
email=email,
phone_number=phone,
password='testpass123',
first_name='Test',
last_name='User',
is_active=True
)
class NotificationServiceTest(TestCase):
"""Тесты сервиса уведомлений"""
def setUp(self):
self.user = create_test_user()
self.category = NotificationCategory.objects.create(
code='test_category',
name='Test Category'
)
self.notification_type = NotificationType.objects.create(
category=self.category,
code='test_type',
name='Test Type',
default_enabled=True
)
def test_create_notification(self):
"""Создание уведомления через сервис"""
notification = NotificationService.create_notification(
recipient=self.user,
notification_type_code='test_type',
title='Test Title',
message='Test Message',
action_url='/test/',
send_immediately=False
)
self.assertIsNotNone(notification)
self.assertEqual(notification.recipient, self.user)
self.assertEqual(notification.title, 'Test Title')
self.assertEqual(notification.message, 'Test Message')
self.assertEqual(notification.action_url, '/test/')
def test_create_notification_invalid_type(self):
"""Создание с несуществующим типом"""
notification = NotificationService.create_notification(
recipient=self.user,
notification_type_code='invalid_type',
title='Test',
message='Test',
send_immediately=False
)
self.assertIsNone(notification)
def test_get_user_settings_creates_if_not_exists(self):
"""Автоматическое создание настроек"""
settings = NotificationService.get_user_settings(
self.user,
self.notification_type
)
self.assertIsNotNone(settings)
self.assertEqual(settings.user, self.user)
self.assertEqual(settings.notification_type, self.notification_type)
self.assertTrue(settings.is_enabled)
def test_mark_as_read(self):
"""Отметка как прочитанное через сервис"""
notification = Notification.objects.create(
recipient=self.user,
notification_type=self.notification_type,
title='Test',
message='Test'
)
success = NotificationService.mark_as_read(notification.id, self.user)
self.assertTrue(success)
notification.refresh_from_db()
self.assertTrue(notification.is_read)
def test_mark_as_read_wrong_user(self):
"""Попытка прочитать чужое уведомление"""
other_user = create_test_user(
email='other@example.com',
phone='+79990000002'
)
notification = Notification.objects.create(
recipient=other_user,
notification_type=self.notification_type,
title='Test',
message='Test'
)
success = NotificationService.mark_as_read(notification.id, self.user)
self.assertFalse(success)
notification.refresh_from_db()
self.assertFalse(notification.is_read)
def test_mark_all_as_read(self):
"""Отметка всех как прочитанные"""
# Создаем несколько уведомлений
for i in range(5):
Notification.objects.create(
recipient=self.user,
notification_type=self.notification_type,
title=f'Test {i}',
message=f'Message {i}'
)
count = NotificationService.mark_all_as_read(self.user)
self.assertEqual(count, 5)
unread = Notification.objects.filter(
recipient=self.user,
is_read=False
).count()
self.assertEqual(unread, 0)

View File

@ -1,175 +0,0 @@
import pytest
from django.contrib.auth import get_user_model
from django.core.cache import cache
from django.conf import settings
pytestmark = pytest.mark.django_db
@pytest.fixture(autouse=True)
def _clear_cache():
cache.clear()
yield
cache.clear()
# На всякий: bakery сможет печь PhoneNumberField, если вдруг где-то всплывёт Employee
@pytest.fixture(autouse=True)
def _bakery_phone_generator():
try:
from model_bakery import generators
from phonenumber_field.modelfields import PhoneNumberField
import itertools
counter = itertools.count(10000)
generators.add(
PhoneNumberField,
lambda: f"+7999{next(counter):07d}"
)
except Exception:
pass
yield
def _mk_user(phone: str, **extra):
User = get_user_model()
defaults = dict(
phone_number=phone,
password=extra.pop("password", "pass"),
email=extra.pop("email", f"{phone}@example.com"),
first_name=extra.pop("first_name", "Test"),
last_name=extra.pop("last_name", "User"),
telegram=extra.pop("telegram", "@test"),
is_active=True, # <<< ВАЖНО: активируем пользователя для тестов
**extra,
)
return User.objects.create_user(**defaults)
@pytest.fixture
def user():
return _mk_user("+79990000001")
@pytest.fixture
def hr_user():
return _mk_user("+79990000002", is_staff=True)
@pytest.fixture
def other_user():
return _mk_user("+79990000003")
@pytest.fixture
def login(client):
def _do(u):
client.force_login(u, backend=settings.AUTHENTICATION_BACKENDS[0])
return client
return _do
@pytest.fixture
def request_obj(user):
from model_bakery import baker
from requests_app.models import Request
return baker.make(
Request,
employee=user,
status=Request.STATUS_PENDING,
type=Request.TYPE_OTHER,
)
# ============================================================================
# Fixtures для тестирования новых фич (recipients/cc/departments)
# ============================================================================
@pytest.fixture
def test_employee(user_factory):
"""Первый тестовый сотрудник"""
return user_factory(email='emp1@test.com')
@pytest.fixture
def test_employee_second(user_factory):
"""Второй тестовый сотрудник"""
return user_factory(email='emp2@test.com')
@pytest.fixture
def test_employee_third(user_factory):
"""Третий тестовый сотрудник"""
return user_factory(email='emp3@test.com')
@pytest.fixture
def test_department(department_factory):
"""Тестовый отдел"""
return department_factory(name='IT Department')
@pytest.fixture
def test_department_second(department_factory):
"""Второй тестовый отдел"""
return department_factory(name='HR Department')
@pytest.fixture
def test_staff_user(user_factory):
"""Staff пользователь"""
return user_factory(email='staff@test.com', staff=True)
@pytest.fixture
def create_request_helper(db):
"""Хелпер для создания заявки с получателями"""
from requests_app.models import Request
from requests_app.enums import RequestStatus, RequestType
def _create(employee, recipients=None, cc_users=None, departments=None,
sent_to_all=False, **kwargs):
defaults = {
'type': RequestType.OTHER,
'title': 'Test request',
'status': RequestStatus.PENDING
}
defaults.update(kwargs)
request = Request.objects.create(
employee=employee,
sent_to_all_department=sent_to_all,
**defaults
)
if recipients:
if not isinstance(recipients, list):
recipients = [recipients]
request.recipients.set(recipients)
if cc_users:
if not isinstance(cc_users, list):
cc_users = [cc_users]
request.cc_users.set(cc_users)
if departments:
if not isinstance(departments, list):
departments = [departments]
request.departments.set(departments)
return request
return _create
@pytest.fixture
def global_viewer(user_factory):
"""Пользователь с глобальным правом can_view_all_requests"""
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from requests_app.models import Request
user = user_factory(email='global@test.com')
# Получаем или создаем permission
ct = ContentType.objects.get_for_model(Request)
perm, _ = Permission.objects.get_or_create(
content_type=ct,
codename='can_view_all_requests',
defaults={'name': 'Можно просмотреть все заявления'}
)
user.user_permissions.add(perm)
return user

View File

@ -1,45 +0,0 @@
import pytest
from django.urls import reverse
from model_bakery import baker
from datetime import timedelta
from django.utils import timezone
pytestmark = pytest.mark.django_db
def test_my_requests_filters_by_date_status_type(client, user, hr_user, login):
from requests_app.models import Request
from requests_app.enums import RequestStatus, RequestType
login(user)
now = timezone.now()
r1 = baker.make(
Request,
employee=user,
status=RequestStatus.PENDING,
type=RequestType.VACATION,
created_at=now - timedelta(days=10),
)
r2 = baker.make(
Request,
employee=user,
status=RequestStatus.PENDING, # сначала pending
type=RequestType.SICK_LEAVE,
created_at=now - timedelta(days=2),
)
# корректно апрувим (заполнит approver и decided_at)
r2.approve(by_user=hr_user)
url = reverse("requests:request_list")
params = {
"status": RequestStatus.APPROVED,
"type": RequestType.SICK_LEAVE,
"from": (now - timedelta(days=5)).date().isoformat(),
"to": now.date().isoformat(),
}
resp = client.get(url, params)
assert resp.status_code == 200
page = resp.context["page"]
objs = list(page.object_list)
assert {o.pk for o in objs} == {r2.pk}

View File

@ -1,195 +0,0 @@
import pytest
from django.core.files.uploadedfile import SimpleUploadedFile
pytestmark = pytest.mark.django_db
def _file(name="f.pdf", content=b"x", content_type="application/pdf"):
return SimpleUploadedFile(name=name, content=content, content_type=content_type)
# -------------------- RequestForm --------------------
def test_request_form_valid_minimal():
from requests_app.forms import RequestForm
from requests_app.models import Request
form = RequestForm(data={"type": Request.TYPE_OTHER, "comment": "ok"})
assert form.is_valid()
def test_request_form_date_reverse_error():
from requests_app.forms import RequestForm
form = RequestForm(
data={"type": "other", "date_from": "2025-01-10", "date_to": "2025-01-01"}
)
assert not form.is_valid()
assert "date_to" in form.errors
def test_request_form_attachment_magic_missing_requires_both_ext_and_ct(monkeypatch):
"""
Когда python-magic недоступен (_sniff_mime -> None), нужен И корректный content_type,
И корректное расширение.
"""
from requests_app import forms as F
# эмулируем отсутствие python-magic
monkeypatch.setattr(F, "_sniff_mime", lambda f: None)
# валидный случай: jpeg + .jpg
form_ok = F.RequestForm(
data={"type": "other"},
files={
"attachment": _file(
name="pic.jpg", content=b"abc", content_type="image/jpeg"
)
},
)
assert form_ok.is_valid()
# невалидный: image/jpeg, но расширение .exe
form_bad = F.RequestForm(
data={"type": "other"},
files={
"attachment": _file(name="a.exe", content=b"abc", content_type="image/jpeg")
},
)
assert not form_bad.is_valid()
assert "attachment" in form_bad.errors
def test_request_form_attachment_magic_detects_mime_and_ext(monkeypatch):
"""
Если mime определён (эмулируем через _sniff_mime), он должен быть из whitelist,
и расширение тоже из whitelist.
"""
from requests_app import forms as F
# определяем как PDF — ок только с .pdf
monkeypatch.setattr(F, "_sniff_mime", lambda f: "application/pdf")
# валидный
form_ok = F.RequestForm(
data={"type": "other"},
files={
"attachment": _file(
name="file.pdf",
content=b"%PDF-1.4",
content_type="application/octet-stream",
)
},
)
assert form_ok.is_valid()
# неверное расширение при корректном mime
form_bad = F.RequestForm(
data={"type": "other"},
files={
"attachment": _file(
name="file.png",
content=b"%PDF-1.4",
content_type="application/octet-stream",
)
},
)
assert not form_bad.is_valid()
assert "attachment" in form_bad.errors
def test_request_form_attachment_size_limit(monkeypatch):
from requests_app import forms as F
# уменьшаем порог, чтобы не плодить большие буферы
monkeypatch.setattr(F, "MAX_ATTACHMENT_SIZE", 5)
big = SimpleUploadedFile("a.pdf", b"123456", content_type="application/pdf")
form = F.RequestForm(data={"type": "other"}, files={"attachment": big})
assert not form.is_valid()
assert "attachment" in form.errors
# -------------------- RequestStatusForm --------------------
def test_status_form_requires_user_kwarg():
from requests_app.forms import RequestStatusForm
from requests_app.models import Request
with pytest.raises(ValueError):
RequestStatusForm(
data={"status": Request.STATUS_APPROVED}, instance=Request(), user=None
)
def test_status_form_allowed_statuses_by_role(user, hr_user):
from requests_app.forms import RequestStatusForm
from requests_app.models import Request
# non-staff: только approved/rejected
form_user = RequestStatusForm(user=user, instance=Request())
values_user = {v for v, _ in form_user.fields["status"].choices}
assert values_user == {Request.STATUS_APPROVED, Request.STATUS_REJECTED}
# staff/HR: плюс cancelled
form_hr = RequestStatusForm(user=hr_user, instance=Request())
values_hr = {v for v, _ in form_hr.fields["status"].choices}
assert values_hr == {
Request.STATUS_APPROVED,
Request.STATUS_REJECTED,
Request.STATUS_CANCELLED,
}
def test_status_form_clean_status_disallowed_for_role_raises(user):
from requests_app.forms import RequestStatusForm
from requests_app.models import Request
form = RequestStatusForm(
data={"status": Request.STATUS_CANCELLED}, user=user, instance=Request()
)
assert not form.is_valid()
assert "status" in form.errors
def test_status_form_cannot_change_final_status(hr_user):
from requests_app.forms import RequestStatusForm
from requests_app.models import Request
# финальная заявка
r = Request(status=Request.STATUS_APPROVED)
form = RequestStatusForm(
data={"status": Request.STATUS_REJECTED}, user=hr_user, instance=r
)
assert not form.is_valid()
assert "status" in form.errors
def test_status_form_clean_attachment_uses_common_validator(monkeypatch, hr_user):
from requests_app import forms as F
from requests_app.models import Request
called = {"ok": False}
def _spy(self, f): # signature совпадает с методом
called["ok"] = True
monkeypatch.setattr(
F.RequestForm, "_validate_attachment_common", _spy, raising=True
)
form = F.RequestStatusForm(
data={"status": Request.STATUS_APPROVED},
files={
"attachment": _file(
name="x.pdf", content=b"pdf", content_type="application/pdf"
)
},
user=hr_user,
instance=Request(),
)
# валидность не важна — нам важно, что вызвался общий валидатор
form.is_valid()
assert called["ok"] is True

View File

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

View File

@ -1,35 +0,0 @@
import pytest
from django.urls import reverse
from model_bakery import baker
pytestmark = pytest.mark.django_db
def test_my_requests_shows_only_own(client, user, other_user, login):
from requests_app.models import Request
mine = baker.make(Request, employee=user, status=Request.STATUS_PENDING, _quantity=2)
other = baker.make(Request, employee=other_user, status=Request.STATUS_PENDING, _quantity=3)
login(user)
resp = client.get(reverse("requests:request_list"))
assert resp.status_code == 200
page = resp.context["page"]
ids = {obj.pk for obj in page.object_list}
assert set(r.pk for r in mine).issubset(ids)
assert not any(r.pk in ids for r in other)
def test_all_requests_requires_hr(client, user, hr_user, login):
# гость → редирект на логин
resp = client.get(reverse("requests:request_list"))
assert resp.status_code in (302, 303)
# не-HR → тоже редирект на логин (так работает user_passes_test без raise_exception)
login(user)
resp = client.get(reverse("requests:request_list"))
assert resp.status_code in (302, 303)
# HR → ок
login(hr_user)
resp = client.get(reverse("requests:request_list"))
assert resp.status_code == 200

View File

@ -1,63 +0,0 @@
import pytest
from django.urls import reverse
from django import forms
pytestmark = pytest.mark.django_db
def test_request_process_approve_and_soft_fields(client, hr_user, request_obj, login, monkeypatch):
from requests_app.models import Request
login(hr_user)
import requests_app.views as v
class _FormStub(forms.Form):
status = forms.CharField(required=False)
comment = forms.CharField(required=False)
def __init__(self, data=None, files=None, instance=None, user=None):
super().__init__(data=data, files=files)
self.instance = instance
self.user = user
self.changed_data = [k for k in ("comment", "attachment") if data and k in data]
def is_valid(self):
self.cleaned_data = {
"status": self.data.get("status"),
"comment": self.data.get("comment"),
}
return True
monkeypatch.setattr(v, "RequestStatusForm", _FormStub)
url = reverse("requests_app:request_process", args=[request_obj.pk])
payload = {"status": Request.STATUS_APPROVED, "comment": "ok-comment"}
resp = client.post(url, payload, follow=True)
assert resp.status_code in (200, 302, 303)
request_obj.refresh_from_db()
assert request_obj.status == Request.STATUS_APPROVED
assert getattr(request_obj, "comment", None) == "ok-comment"
def test_request_process_invalid_status(client, hr_user, request_obj, login, monkeypatch):
from requests_app.models import Request
login(hr_user)
import requests_app.views as v
class _FormStub(forms.Form):
status = forms.CharField(required=False)
comment = forms.CharField(required=False)
def __init__(self, data=None, files=None, instance=None, user=None):
super().__init__(data=data, files=files)
self.instance = instance
self.user = user
self.changed_data = []
def is_valid(self):
self.cleaned_data = {"status": self.data.get("status"), "comment": self.data.get("comment")}
return True
monkeypatch.setattr(v, "RequestStatusForm", _FormStub)
url = reverse("requests_app:request_process", args=[request_obj.pk])
old_status = request_obj.status
for bad in (Request.STATUS_DRAFT, Request.STATUS_PENDING):
resp = client.post(url, {"status": bad})
assert resp.status_code == 200 # остаёмся на форме с ошибкой
request_obj.refresh_from_db()
assert request_obj.status == old_status # статус не изменился

View File

@ -1,24 +0,0 @@
import time
import pytest
from django.urls import reverse
pytestmark = pytest.mark.django_db
def test_request_create_cache_rate_limit_fixed_window(client, user, login, monkeypatch, settings):
login(user)
import requests_app.views as v
monkeypatch.setattr(v, "RATE_LIMIT_AVAILABLE", False)
url = reverse("requests_app:request_create")
for _ in range(15):
assert client.get(url).status_code == 200
assert client.get(url).status_code == 429
real_time = time.time # сохраняем оригинал
monkeypatch.setattr(v.time, "time", lambda: real_time() + 61)
assert client.get(url).status_code == 200

View File

@ -1,17 +0,0 @@
import pytest
from django.urls import reverse
pytestmark = pytest.mark.django_db
def test_safe_back_url_on_delete_external_next_blocked(client, user, request_obj, login):
# Создадим собственный комментарий, чтобы было что удалять
from requests_app.models import RequestComment
comment = RequestComment.objects.create(request=request_obj, author=user, text="x")
login(user)
url = reverse("requests_app:request_comment_delete", args=[request_obj.pk, comment.pk])
resp = client.post(url + "?next=https://attacker.tld/path")
assert resp.status_code in (302, 303)
# Нельзя увести наружу
assert "attacker.tld" not in resp["Location"]

View File

@ -1,301 +0,0 @@
"""
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

View File

@ -1,503 +0,0 @@
"""
Тесты для двунаправленной загрузки сообщений (Telegram-style)
Проверяемые сценарии:
1. Загрузка последних сообщений (стандартный GET)
2. Загрузка старых сообщений (before_id)
3. Загрузка новых сообщений (after_id) - НОВОЕ
4. Загрузка вокруг даты (loadAround) - НОВОЕ
5. Корректность has_more_after флага - НОВОЕ
6. Корректность boundaries после каждой загрузки
Запуск:
cd backend
.venv/Scripts/python -m pytest tests/test_bidirectional_chat_loading.py -v
"""
import pytest
from datetime import datetime, timedelta
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils import timezone
from rest_framework.test import APIClient
from rest_framework import status
from communications.models import Chat, Message
User = get_user_model()
@pytest.fixture
def api_client():
"""API клиент для тестов"""
return APIClient()
@pytest.fixture
def test_user(db):
"""Тестовый пользователь"""
user = User.objects.create_user(
email='test@example.com',
password='testpass123',
phone_number='+79991234567',
first_name='Test',
last_name='User',
send_activation_email=False
)
# Активируем пользователя для тестов
user.email_verified = True
user.is_active = True
user.save()
return user
@pytest.fixture
def test_chat(db, test_user):
"""Тестовый чат"""
chat = Chat.objects.create(
name='Test Chat',
type='global'
)
chat.participants.add(test_user)
return chat
@pytest.fixture
def test_messages(db, test_chat, test_user):
"""
Создаем 100 сообщений с разными датами для тестирования:
- 30 сообщений за 1 января 2026
- 40 сообщений за 5 января 2026
- 30 сообщений за 10 января 2026
"""
messages = []
# 1 января - 30 сообщений
base_date = timezone.make_aware(datetime(2026, 1, 1, 12, 0, 0))
for i in range(30):
msg = Message.objects.create(
chat=test_chat,
author=test_user,
content=f'Message from Jan 1 - {i}',
created_at=base_date + timedelta(minutes=i)
)
messages.append(msg)
# 5 января - 40 сообщений
base_date = timezone.make_aware(datetime(2026, 1, 5, 12, 0, 0))
for i in range(40):
msg = Message.objects.create(
chat=test_chat,
author=test_user,
content=f'Message from Jan 5 - {i}',
created_at=base_date + timedelta(minutes=i)
)
messages.append(msg)
# 10 января - 30 сообщений
base_date = timezone.make_aware(datetime(2026, 1, 10, 12, 0, 0))
for i in range(30):
msg = Message.objects.create(
chat=test_chat,
author=test_user,
content=f'Message from Jan 10 - {i}',
created_at=base_date + timedelta(minutes=i)
)
messages.append(msg)
return messages
class TestStandardLoading:
"""Тесты стандартной загрузки (последние сообщения)"""
def test_load_latest_messages(self, api_client, test_user, test_chat, test_messages):
"""Тест 1: Загрузка последних сообщений без параметров"""
# Логинимся через Django session (т.к. view использует @login_required)
api_client.force_login(test_user)
url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
response = api_client.get(url)
assert response.status_code == status.HTTP_200_OK
data = response.json()
# Должны получить последние сообщения (по умолчанию 30)
assert len(data['messages']) == 30
assert data['has_more'] is True or data.get('has_more_before') is True
# Для последних сообщений has_more_after не возвращается (это backwards loading)
# Проверяем что это действительно последние сообщения (10 января)
first_msg = data['messages'][0]
assert 'Jan 10' in first_msg['content']
def test_load_with_limit(self, api_client, test_user, test_chat, test_messages):
"""Тест 2: Загрузка с кастомным лимитом"""
api_client.force_login(test_user)
url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
response = api_client.get(url, {'limit': 50})
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert len(data['messages']) == 50
assert data['has_more'] is True
class TestHistoryLoading:
"""Тесты загрузки истории (старые сообщения)"""
def test_load_before_id(self, api_client, test_user, test_chat, test_messages):
"""Тест 3: Загрузка сообщений ПЕРЕД указанным ID (история)"""
api_client.force_login(test_user)
url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
# Сначала загружаем последние
response = api_client.get(url)
data = response.json()
oldest_id = data['messages'][0]['id']
# Теперь загружаем историю
response = api_client.get(url, {'before_id': oldest_id, 'limit': 20})
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert len(data['messages']) <= 20
# Все сообщения должны быть старше oldest_id
for msg in data['messages']:
assert msg['id'] < oldest_id
def test_load_before_timestamp(self, api_client, test_user, test_chat, test_messages):
"""Тест 4: Загрузка сообщений перед timestamp"""
api_client.force_login(test_user)
url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
# Загружаем сообщения перед 8 января 2026
timestamp = int(datetime(2026, 1, 8, 0, 0, 0).timestamp() * 1000)
response = api_client.get(url, {'before_ts': timestamp, 'limit': 30})
assert response.status_code == status.HTTP_200_OK
data = response.json()
# Должны получить сообщения только за 1 и 5 января
for msg in data['messages']:
assert 'Jan 1' in msg['content'] or 'Jan 5' in msg['content']
assert 'Jan 10' not in msg['content']
class TestNewerLoading:
"""Тесты загрузки НОВЫХ сообщений (after_id) - КЛЮЧЕВАЯ ФУНКЦИОНАЛЬНОСТЬ"""
def test_load_after_id(self, api_client, test_user, test_chat, test_messages):
"""Тест 5: Загрузка сообщений ПОСЛЕ указанного ID (новые)"""
api_client.force_login(test_user)
url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
# Загружаем сообщения за 1 января (первые 30)
oldest_message = test_messages[29] # Последнее сообщение 1 января
# Загружаем сообщения ПОСЛЕ этого ID
response = api_client.get(url, {
'after_id': oldest_message.id,
'limit': 20
})
assert response.status_code == status.HTTP_200_OK
data = response.json()
# Должны получить сообщения за 5 января
assert len(data['messages']) == 20
# Все сообщения должны быть НОВЕЕ oldest_message.id
for msg in data['messages']:
assert msg['id'] > oldest_message.id
# Проверяем что это сообщения за 5 января
assert 'Jan 5' in data['messages'][0]['content']
# Должен быть флаг has_more_after (еще есть сообщения за 10 января)
assert data['has_more_after'] is True
def test_load_after_timestamp(self, api_client, test_user, test_chat, test_messages):
"""Тест 6: Загрузка сообщений после timestamp"""
api_client.force_login(test_user)
url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
# Загружаем сообщения после 3 января 2026
# Используем сообщение из базы чтобы получить правильный timestamp
# test_messages[0-29] = Jan 1, test_messages[30-69] = Jan 5
# Берем timestamp последнего сообщения Jan 1
boundary_message = test_messages[29] # Последнее сообщение Jan 1
first_jan5 = test_messages[30] # Первое сообщение Jan 5
# Для отладки
print(f"\nBoundary message (Jan 1): id={boundary_message.id}, created_at={boundary_message.created_at}")
print(f"First Jan 5 message: id={first_jan5.id}, created_at={first_jan5.created_at}")
timestamp = int(boundary_message.created_at.timestamp() * 1000)
print(f"Timestamp sent: {timestamp}")
response = api_client.get(url, {'after_ts': timestamp, 'limit': 30})
assert response.status_code == status.HTTP_200_OK
data = response.json()
print(f"Got {len(data['messages'])} messages")
if data['messages']:
print(f"First message: {data['messages'][0]['content']}")
# Должны получить первые 30 сообщений после Jan 1 (за 5 января)
# Но API выбирает строго по __gt, может быть рассинхрон
# Корректируем тест: просто проверяем что получили сообщения
assert len(data['messages']) > 0
def test_no_more_after_flag(self, api_client, test_user, test_chat, test_messages):
"""Тест 7: has_more_after должен быть False для последних сообщений"""
api_client.force_login(test_user)
url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
# Загружаем с конца (по умолчанию - это загрузка истории, не forward loading)
response = api_client.get(url, {'limit': 50})
data = response.json()
# Стандартный запрос (без after_id/after_ts) возвращает has_more_before
# has_more_after не присутствует в ответе при backwards loading
assert 'has_more_before' in data
assert data['has_more'] is True
class TestLoadAround:
"""Тесты загрузки вокруг даты (loadAround) - НОВАЯ ФУНКЦИОНАЛЬНОСТЬ"""
def test_load_around_date(self, api_client, test_user, test_chat, test_messages):
"""Тест 8: Загрузка сообщений вокруг даты (5 января)"""
api_client.force_login(test_user)
url = reverse('api:v1:chat_messages_around', kwargs={'pk': test_chat.pk})
# Найдем ID сообщения из середины 5 января
# test_messages = 30 (Jan 1) + 40 (Jan 5) + 30 (Jan 10)
# Jan 5 начинается с индекса 30, середина ~50
anchor_message = test_messages[50]
response = api_client.get(url, {
'around_id': anchor_message.id,
'limit': 30
})
assert response.status_code == status.HTTP_200_OK
data = response.json()
# Должны получить сообщения вокруг 5 января
assert len(data['messages']) > 0
assert 'anchor_id' in data
assert 'has_more_before' in data
assert 'has_more_after' in data
# Должны быть сообщения за 5 января
jan5_messages = [msg for msg in data['messages'] if 'Jan 5' in msg['content']]
assert len(jan5_messages) > 0
# has_more_before должен быть True (есть сообщения за 1 января)
assert data['has_more_before'] is True
# has_more_after должен быть True (есть сообщения за 10 января)
assert data['has_more_after'] is True
def test_load_around_earliest_date(self, api_client, test_user, test_chat, test_messages):
"""Тест 9: Загрузка вокруг самой ранней даты"""
api_client.force_login(test_user)
url = reverse('api:v1:chat_messages_around', kwargs={'pk': test_chat.pk})
# Первое сообщение (1 января)
anchor_message = test_messages[0]
response = api_client.get(url, {'around_id': anchor_message.id, 'limit': 30})
assert response.status_code == status.HTTP_200_OK
data = response.json()
# has_more_before должен быть False (это самые ранние сообщения)
assert data['has_more_before'] is False
# has_more_after должен быть True (есть сообщения за 5 и 10 января)
assert data['has_more_after'] is True
def test_load_around_latest_date(self, api_client, test_user, test_chat, test_messages):
"""Тест 10: Загрузка вокруг самой поздней даты"""
api_client.force_login(test_user)
url = reverse('api:v1:chat_messages_around', kwargs={'pk': test_chat.pk})
# Последнее сообщение (10 января)
anchor_message = test_messages[-1]
response = api_client.get(url, {'around_id': anchor_message.id, 'limit': 30})
assert response.status_code == status.HTTP_200_OK
data = response.json()
# has_more_before должен быть True (есть история)
assert data['has_more_before'] is True
# has_more_after должен быть False (это последние сообщения)
assert data['has_more_after'] is False
class TestBidirectionalFlow:
"""Тесты полного цикла двунаправленной загрузки"""
def test_full_bidirectional_scenario(self, api_client, test_user, test_chat, test_messages):
"""
Тест 11: Полный сценарий использования
1. Прыжок на 5 января (loadAround)
2. Загрузка старых сообщений (loadHistory)
3. Загрузка новых сообщений (loadNewer)
"""
api_client.force_login(test_user)
messages_url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
around_url = reverse('api:v1:chat_messages_around', kwargs={'pk': test_chat.pk})
# Шаг 1: Прыгаем на 5 января (середина)
# test_messages[50] - это сообщение из середины 5 января
anchor_message = test_messages[50]
response = api_client.get(around_url, {'around_id': anchor_message.id, 'limit': 20})
data = response.json()
assert response.status_code == status.HTTP_200_OK
assert data['has_more_before'] is True
assert data['has_more_after'] is True
oldest_id = data['messages'][0]['id']
newest_id = data['messages'][-1]['id']
# Шаг 2: Загружаем старые сообщения (вверх)
response = api_client.get(messages_url, {
'before_id': oldest_id,
'limit': 20
})
history_data = response.json()
assert response.status_code == status.HTTP_200_OK
assert len(history_data['messages']) > 0
# Все сообщения старше oldest_id
for msg in history_data['messages']:
assert msg['id'] < oldest_id
# Шаг 3: Загружаем новые сообщения (вниз)
response = api_client.get(messages_url, {
'after_id': newest_id,
'limit': 20
})
newer_data = response.json()
assert response.status_code == status.HTTP_200_OK
assert len(newer_data['messages']) > 0
# Все сообщения новее newest_id
for msg in newer_data['messages']:
assert msg['id'] > newest_id
class TestEdgeCases:
"""Тесты граничных случаев"""
def test_empty_chat(self, api_client, test_user, test_chat):
"""Тест 12: Пустой чат"""
api_client.force_login(test_user)
url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
response = api_client.get(url)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert len(data['messages']) == 0
assert data['has_more'] is False
# Стандартный запрос возвращает has_more_before, а не has_more_after
assert data['has_more_before'] is False
def test_invalid_after_id(self, api_client, test_user, test_chat, test_messages):
"""Тест 13: Несуществующий after_id - должен загрузить с начала"""
api_client.force_login(test_user)
url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
# Несуществующий after_id - API загружает с начала чата (от старых к новым)
response = api_client.get(url, {'after_id': 999999, 'limit': 30})
assert response.status_code == status.HTTP_200_OK
data = response.json()
# API загружает первые 30 сообщений от начала
assert len(data['messages']) == 30
# Это forward loading, поэтому has_more_after должен быть True
assert data['has_more_after'] is True
def test_after_id_at_end(self, api_client, test_user, test_chat, test_messages):
"""Тест 14: after_id указывает на последнее сообщение"""
api_client.force_login(test_user)
url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
# Получаем ID последнего сообщения
last_message_id = test_messages[-1].id
response = api_client.get(url, {'after_id': last_message_id})
assert response.status_code == status.HTTP_200_OK
data = response.json()
# Нет сообщений после последнего
assert len(data['messages']) == 0
assert data['has_more_after'] is False
class TestPagination:
"""Тесты пагинации при двунаправленной загрузке"""
def test_pagination_consistency(self, api_client, test_user, test_chat, test_messages):
"""Тест 15: Согласованность пагинации в обоих направлениях"""
api_client.force_login(test_user)
url = reverse('api:v1:chat_messages', kwargs={'pk': test_chat.pk})
# Загружаем средний батч
middle_message = test_messages[50]
# Загружаем после
response_after = api_client.get(url, {
'after_id': middle_message.id,
'limit': 10
})
# Загружаем перед
response_before = api_client.get(url, {
'before_id': middle_message.id,
'limit': 10
})
after_data = response_after.json()
before_data = response_before.json()
# Проверяем что результаты не пересекаются
after_ids = {msg['id'] for msg in after_data['messages']}
before_ids = {msg['id'] for msg in before_data['messages']}
assert len(after_ids & before_ids) == 0 # Нет пересечений
assert middle_message.id not in after_ids
assert middle_message.id not in before_ids
# ==================== Маркеры для запуска ====================
@pytest.mark.django_db
class TestBidirectionalLoadingIntegration:
"""Интеграционные тесты полного цикла"""
pass
if __name__ == '__main__':
pytest.main([__file__, '-v', '--tb=short'])

View File

@ -1,253 +0,0 @@
"""
Тесты для статических файлов (CSS/JS)
Проверяют:
1. Валидность структуры CSS файлов
2. Корректность ES6 модулей JavaScript
3. Наличие обязательных файлов
4. Отсутствие синтаксических ошибок
"""
from pathlib import Path
from django.test import TestCase
from django.conf import settings
class StaticFilesStructureTest(TestCase):
"""Тесты структуры статических файлов"""
def setUp(self):
self.static_root = Path(settings.BASE_DIR) / 'static'
self.css_root = self.static_root / 'css'
self.js_root = self.static_root / 'js'
def test_css_directories_exist(self):
"""Проверка существования CSS директорий"""
self.assertTrue(
self.css_root.exists(),
"CSS директория должна существовать"
)
self.assertTrue(
(self.css_root / 'components').exists(),
"CSS components директория должна существовать"
)
self.assertTrue(
(self.css_root / 'pages').exists(),
"CSS pages директория должна существовать"
)
def test_js_directories_exist(self):
"""Проверка существования JS директорий"""
self.assertTrue(
self.js_root.exists(),
"JS директория должна существовать"
)
self.assertTrue(
(self.js_root / 'utils').exists(),
"JS utils директория должна существовать"
)
self.assertTrue(
(self.js_root / 'components').exists(),
"JS components директория должна существовать"
)
def test_index_files_exist(self):
"""Проверка существования index файлов"""
self.assertTrue(
(self.css_root / 'components' / 'index.css').exists(),
"CSS components/index.css должен существовать"
)
self.assertTrue(
(self.js_root / 'utils' / 'index.js').exists(),
"JS utils/index.js должен существовать"
)
self.assertTrue(
(self.js_root / 'components' / 'index.js').exists(),
"JS components/index.js должен существовать"
)
def test_variables_css_exists(self):
"""Проверка существования variables.css"""
variables_file = self.css_root / 'variables.css'
self.assertTrue(
variables_file.exists(),
"variables.css должен существовать"
)
# Проверка содержания основных переменных
content = variables_file.read_text(encoding='utf-8')
required_vars = [
'--navbar-height',
'--sidebar-width',
'--feed-radius',
'--shadow-md',
'--transition-base'
]
for var in required_vars:
self.assertIn(
var,
content,
f"Переменная {var} должна быть определена в variables.css"
)
class CSSFilesValidityTest(TestCase):
"""Тесты валидности CSS файлов"""
def setUp(self):
self.css_root = Path(settings.BASE_DIR) / 'static' / 'css'
def test_css_files_syntax(self):
"""Базовая проверка синтаксиса CSS файлов"""
css_files = list(self.css_root.rglob('*.css'))
for css_file in css_files:
with self.subTest(file=css_file.name):
content = css_file.read_text(encoding='utf-8')
# Проверка парности фигурных скобок
open_braces = content.count('{')
close_braces = content.count('}')
self.assertEqual(
open_braces,
close_braces,
f"{css_file.name}: несбалансированные фигурные скобки"
)
# Проверка что нет незакрытых комментариев
# Простая проверка: /* должно быть равно */
open_comments = content.count('/*')
close_comments = content.count('*/')
self.assertEqual(
open_comments,
close_comments,
f"{css_file.name}: незакрытые комментарии"
)
def test_css_files_encoding(self):
"""Проверка кодировки CSS файлов (UTF-8)"""
css_files = list(self.css_root.rglob('*.css'))
for css_file in css_files:
with self.subTest(file=css_file.name):
try:
css_file.read_text(encoding='utf-8')
except UnicodeDecodeError:
msg = f"{css_file.name}: неверная кодировка (не UTF-8)"
self.fail(msg)
class JSFilesValidityTest(TestCase):
"""Тесты валидности JavaScript файлов"""
def setUp(self):
self.js_root = Path(settings.BASE_DIR) / 'static' / 'js'
def test_js_files_are_modules(self):
"""Проверка что JS файлы используют ES6 модули"""
js_files = list(self.js_root.rglob('*.js'))
for js_file in js_files:
with self.subTest(file=js_file.name):
content = js_file.read_text(encoding='utf-8')
# Проверка что файл содержит export (ES6 модуль)
self.assertTrue(
'export' in content,
f"{js_file.name}: должен содержать export для ES6 модуля"
)
def test_js_files_syntax_basic(self):
"""Базовая проверка синтаксиса JS файлов"""
js_files = list(self.js_root.rglob('*.js'))
for js_file in js_files:
with self.subTest(file=js_file.name):
content = js_file.read_text(encoding='utf-8')
# Проверка парности фигурных скобок
open_braces = content.count('{')
close_braces = content.count('}')
self.assertEqual(
open_braces,
close_braces,
f"{js_file.name}: несбалансированные фигурные скобки"
)
# Проверка парности круглых скобок
open_parens = content.count('(')
close_parens = content.count(')')
self.assertEqual(
open_parens,
close_parens,
f"{js_file.name}: несбалансированные круглые скобки"
)
# Проверка парности квадратных скобок
open_brackets = content.count('[')
close_brackets = content.count(']')
self.assertEqual(
open_brackets,
close_brackets,
f"{js_file.name}: несбалансированные квадратные скобки"
)
def test_js_files_encoding(self):
"""Проверка кодировки JS файлов (UTF-8)"""
js_files = list(self.js_root.rglob('*.js'))
for js_file in js_files:
with self.subTest(file=js_file.name):
try:
js_file.read_text(encoding='utf-8')
except UnicodeDecodeError:
self.fail(f"{js_file.name}: неверная кодировка (не UTF-8)")
class IndexFilesTest(TestCase):
"""Тесты для index файлов"""
def test_css_index_imports(self):
"""Проверка что CSS index.css готов к импорту компонентов"""
index_file = (
Path(settings.BASE_DIR) / 'static' / 'css' /
'components' / 'index.css'
)
content = index_file.read_text(encoding='utf-8')
# Проверка что есть комментарии о подключении
self.assertIn(
'@import',
content,
"index.css должен содержать примеры @import"
)
def test_js_utils_index_exports(self):
"""Проверка что JS utils/index.js готов к экспорту утилит"""
index_file = (
Path(settings.BASE_DIR) / 'static' / 'js' /
'utils' / 'index.js'
)
content = index_file.read_text(encoding='utf-8')
# Проверка что есть export
self.assertIn(
'export',
content,
"utils/index.js должен содержать export"
)
def test_js_components_index_exports(self):
"""Проверка что JS components/index.js готов к экспорту"""
index_file = (
Path(settings.BASE_DIR) / 'static' / 'js' /
'components' / 'index.js'
)
content = index_file.read_text(encoding='utf-8')
# Проверка что есть export
self.assertIn(
'export',
content,
"components/index.js должен содержать export"
)

View File

@ -1,164 +0,0 @@
"""
Тесты для проверки синтаксиса компонентов шаблонов.
Проверяет, что компоненты не содержат тегов, принадлежащих родительским layout'ам.
"""
import os
from pathlib import Path
from django.test import TestCase
from django.template import loader, TemplateSyntaxError
class TemplateComponentsTestCase(TestCase):
"""Тесты синтаксиса компонентов шаблонов"""
# Список всех компонентов для проверки
COMPONENTS = [
# Компоненты employees/department
'employees/components/department_header.html',
'employees/components/department_info.html',
'employees/components/department_team_circle.html',
'employees/components/department_sidebar.html',
'employees/components/department_modals.html',
'employees/components/department_scripts.html',
'employees/components/department_styles.html',
# Компоненты employees/employee_form
'employees/components/employee_form_personal.html',
'employees/components/employee_form_contact.html',
'employees/components/employee_form_position.html',
'employees/components/employee_form_actions.html',
'employees/components/employee_form_scripts.html',
'employees/_employee_edit.html',
]
# Главные шаблоны, которые используют компоненты
MAIN_TEMPLATES = [
'employees/department_detail.html',
'includes/rightbar_calendar.html',
]
# Недопустимые теги в компонентах (они должны быть только в layout'ах)
FORBIDDEN_TAGS = [
'{% extends ',
'{% block content %}',
'{% block extra_css %}',
'{% block extra_js %}',
]
def test_components_syntax(self):
"""Проверка синтаксиса всех компонентов через Django template loader"""
errors = []
for template_path in self.COMPONENTS:
try:
loader.get_template(template_path)
except TemplateSyntaxError as e:
errors.append(f"{template_path}: {e}")
except Exception as e:
errors.append(f"{template_path}: Unexpected error - {e}")
if errors:
error_msg = "\n".join([f" - {err}" for err in errors])
self.fail(f"Found {len(errors)} template syntax errors:\n{error_msg}")
def test_main_templates_syntax(self):
"""Проверка синтаксиса главных шаблонов"""
errors = []
for template_path in self.MAIN_TEMPLATES:
try:
loader.get_template(template_path)
except TemplateSyntaxError as e:
errors.append(f"{template_path}: {e}")
except Exception as e:
errors.append(f"{template_path}: Unexpected error - {e}")
if errors:
error_msg = "\n".join([f" - {err}" for err in errors])
self.fail(f"Found {len(errors)} main template syntax errors:\n{error_msg}")
def test_components_no_forbidden_tags(self):
"""Проверка, что компоненты не содержат тегов layout'ов"""
from django.conf import settings
templates_dir = Path(settings.BASE_DIR) / 'templates'
errors = []
for template_path in self.COMPONENTS:
file_path = templates_dir / template_path
if not file_path.exists():
continue
content = file_path.read_text(encoding='utf-8')
# Проверка на недопустимые теги
for forbidden_tag in self.FORBIDDEN_TAGS:
if forbidden_tag in content:
errors.append(f"{template_path}: contains forbidden tag '{forbidden_tag}'")
if errors:
error_msg = "\n".join([f" - {err}" for err in errors])
self.fail(f"Found {len(errors)} components with forbidden layout tags:\n{error_msg}")
def test_components_no_orphan_endblock(self):
"""Проверка, что компоненты не содержат одиноких {% endblock %}"""
from django.conf import settings
templates_dir = Path(settings.BASE_DIR) / 'templates'
errors = []
for template_path in self.COMPONENTS:
file_path = templates_dir / template_path
if not file_path.exists():
continue
content = file_path.read_text(encoding='utf-8')
# Проверка на {% endblock %} (должен быть только в layout'ах)
if '{% endblock %}' in content or '{% endblock' in content:
# Получаем номера строк с endblock
lines_with_endblock = [
i + 1 for i, line in enumerate(content.splitlines())
if '{% endblock' in line
]
errors.append(
f"{template_path}: contains orphan {{% endblock %}} on line(s) {lines_with_endblock}"
)
if errors:
error_msg = "\n".join([f" - {err}" for err in errors])
self.fail(f"Found {len(errors)} components with orphan endblock tags:\n{error_msg}")
def test_balanced_tags(self):
"""Проверка баланса открывающих/закрывающих тегов"""
from django.conf import settings
templates_dir = Path(settings.BASE_DIR) / 'templates'
errors = []
tag_pairs = [
('{% if ', '{% endif %}'),
('{% for ', '{% endfor %}'),
('{% with ', '{% endwith %}'),
]
for template_path in self.COMPONENTS:
file_path = templates_dir / template_path
if not file_path.exists():
continue
content = file_path.read_text(encoding='utf-8')
for open_tag, close_tag in tag_pairs:
open_count = content.count(open_tag)
close_count = content.count(close_tag)
if open_count != close_count:
errors.append(
f"{template_path}: unbalanced tags - "
f"{open_count} × '{open_tag}' but {close_count} × '{close_tag}'"
)
if errors:
error_msg = "\n".join([f" - {err}" for err in errors])
self.fail(f"Found {len(errors)} components with unbalanced tags:\n{error_msg}")

View File

@ -1,196 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Тест автоматического расширения textarea</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.test-section {
margin-bottom: 40px;
padding: 20px;
border: 1px solid #dee2e6;
border-radius: 8px;
}
.test-section h3 {
margin-bottom: 15px;
color: #0d6efd;
}
textarea {
resize: none;
overflow-y: auto;
}
.status {
margin-top: 10px;
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
}
.status.success {
background-color: #d1e7dd;
color: #0f5132;
}
.status.pending {
background-color: #fff3cd;
color: #664d03;
}
</style>
</head>
<body>
<h1 class="mb-4">🔧 Тест автоматического расширения textarea</h1>
<div class="alert alert-info">
<strong>Инструкция:</strong> Начните вводить текст в каждое поле. Поля должны автоматически расширяться до максимум 6 строк (144px).
</div>
<!-- Тест 1: Комментарий в ленте -->
<div class="test-section">
<h3>1. Быстрый комментарий в ленте (rows="1")</h3>
<div class="comment-form-quick">
<textarea name="text" class="form-control message-input" rows="1" placeholder="Оставить комментарий…"></textarea>
</div>
<div class="status pending" id="status1">⏳ Ожидание ввода...</div>
</div>
<!-- Тест 2: Комментарий в модальном окне -->
<div class="test-section">
<h3>2. Комментарий в модальном окне (rows="1")</h3>
<div class="comment-form-modal">
<textarea name="text" class="form-control message-input" rows="1" placeholder="Оставить комментарий в модальном окне…"></textarea>
</div>
<div class="status pending" id="status2">⏳ Ожидание ввода...</div>
</div>
<!-- Тест 3: Комментарий к заявке -->
<div class="test-section">
<h3>3. Комментарий к заявке (rows="1")</h3>
<form class="request-comment-form">
<textarea name="text" class="form-control message-input" rows="1" placeholder="Комментарий к заявке…"></textarea>
</form>
<div class="status pending" id="status3">⏳ Ожидание ввода...</div>
</div>
<!-- Тест 4: Чат composer -->
<div class="test-section">
<h3>4. Поле ввода в чате (composer-textarea)</h3>
<textarea class="form-control composer-textarea message-input" placeholder="Написать сообщение…"></textarea>
<div class="status pending" id="status4">⏳ Ожидание ввода...</div>
</div>
<!-- Тест 5: Textarea без автоматического расширения -->
<div class="test-section">
<h3>5. Textarea БЕЗ автоматического расширения (rows="3")</h3>
<textarea class="form-control message-input" rows="3" placeholder="Это поле НЕ должно автоматически расширяться…"></textarea>
<div class="status pending" id="status5">⏳ Должно остаться на 3 строках</div>
</div>
<!-- Тест 6: Динамически добавленный textarea -->
<div class="test-section">
<h3>6. Динамически добавленный textarea</h3>
<button class="btn btn-primary mb-3" id="addTextarea">Добавить textarea</button>
<div id="dynamicContainer"></div>
<div class="status pending" id="status6">⏳ Нажмите кнопку</div>
</div>
<!-- Результаты тестирования -->
<div class="card mt-4">
<div class="card-body">
<h5 class="card-title">📊 Результаты тестирования</h5>
<ul id="results" class="mb-0">
<li>Инициализация модуля...</li>
</ul>
</div>
</div>
<!-- Загружаем модуль -->
<script src="../static/js/components/textareaAutoExpand.js"></script>
<!-- Тестовая логика -->
<script>
// Ждём инициализации модуля
setTimeout(() => {
const results = document.getElementById('results');
results.innerHTML = '';
// Проверяем, что модуль загружен
if (window.textareaAutoExpand) {
results.innerHTML += '<li class="text-success">✅ Модуль textareaAutoExpand загружен</li>';
} else {
results.innerHTML += '<li class="text-danger">❌ Модуль textareaAutoExpand НЕ загружен</li>';
}
// Проверяем инициализацию textarea
const textareas = document.querySelectorAll('textarea[rows="1"], textarea.composer-textarea');
results.innerHTML += `<li>Найдено textarea для автоматического расширения: ${textareas.length}</li>`;
textareas.forEach((ta, idx) => {
if (ta.dataset.autoExpandInit) {
results.innerHTML += `<li class="text-success">✅ Textarea #${idx + 1} инициализирован</li>`;
} else {
results.innerHTML += `<li class="text-warning">⚠️ Textarea #${idx + 1} не инициализирован</li>`;
}
});
// Мониторинг изменений высоты
textareas.forEach((ta, idx) => {
const statusId = `status${idx + 1}`;
const statusEl = document.getElementById(statusId);
if (!statusEl) return;
let lastHeight = ta.offsetHeight;
ta.addEventListener('input', () => {
const currentHeight = ta.offsetHeight;
if (currentHeight !== lastHeight) {
statusEl.className = 'status success';
statusEl.textContent = `✅ Высота изменена: ${lastHeight}px → ${currentHeight}px`;
lastHeight = currentHeight;
if (currentHeight >= 144) {
statusEl.textContent += ' (достигнут максимум 6 строк)';
}
}
});
});
// Тест динамического добавления
document.getElementById('addTextarea').addEventListener('click', () => {
const container = document.getElementById('dynamicContainer');
const textarea = document.createElement('textarea');
textarea.className = 'form-control message-input';
textarea.setAttribute('rows', '1');
textarea.placeholder = 'Динамически добавленное поле…';
container.appendChild(textarea);
setTimeout(() => {
if (textarea.dataset.autoExpandInit) {
document.getElementById('status6').className = 'status success';
document.getElementById('status6').textContent = '✅ MutationObserver работает! Textarea инициализирован автоматически';
} else {
document.getElementById('status6').className = 'status danger';
document.getElementById('status6').textContent = '❌ MutationObserver НЕ работает';
}
}, 500);
});
// Тест textarea с rows="3"
const textarea3 = document.querySelector('textarea[rows="3"]');
textarea3.addEventListener('input', () => {
const status5 = document.getElementById('status5');
if (!textarea3.dataset.autoExpandInit) {
status5.className = 'status success';
status5.textContent = '✅ Корректно: НЕ инициализирован (rows="3")';
} else {
status5.className = 'status danger';
status5.textContent = '❌ Ошибка: инициализирован, хотя не должен был';
}
});
}, 1000);
</script>
</body>
</html>