Remove outdated test files and fixtures
This commit is contained in:
parent
ae340f4d03
commit
624784b788
@ -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. Создать итоговый отчёт
|
||||
@ -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")
|
||||
@ -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]
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
BIN
backend/tests/fixtures/ldap/ldap_test_data/data.mdb
vendored
BIN
backend/tests/fixtures/ldap/ldap_test_data/data.mdb
vendored
Binary file not shown.
BIN
backend/tests/fixtures/ldap/ldap_test_data/lock.mdb
vendored
BIN
backend/tests/fixtures/ldap/ldap_test_data/lock.mdb
vendored
Binary file not shown.
@ -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
|
||||
```
|
||||
@ -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)
|
||||
- [ ] Добавить тесты для групп и департаментов
|
||||
- [ ] Добавить тесты для обновления пользователей
|
||||
- [ ] Протестировать синхронизацию при изменении данных
|
||||
@ -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}")
|
||||
@ -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}")
|
||||
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -1,8 +0,0 @@
|
||||
"""
|
||||
Конфигурация для manual тестов.
|
||||
Все тесты в этой директории автоматически помечаются как manual.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
# Автоматически помечать все тесты в этой директории как manual
|
||||
pytestmark = pytest.mark.manual
|
||||
@ -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()
|
||||
@ -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)
|
||||
@ -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")
|
||||
@ -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()
|
||||
@ -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()
|
||||
@ -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()
|
||||
@ -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())
|
||||
@ -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)
|
||||
@ -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()
|
||||
@ -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()
|
||||
@ -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()
|
||||
@ -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()
|
||||
@ -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)
|
||||
@ -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}")
|
||||
@ -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)
|
||||
@ -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()
|
||||
@ -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()
|
||||
@ -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()
|
||||
@ -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]
|
||||
@ -1 +0,0 @@
|
||||
# Notifications tests package
|
||||
@ -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)
|
||||
@ -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'
|
||||
)
|
||||
@ -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)
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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 # статус не изменился
|
||||
@ -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
|
||||
@ -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"]
|
||||
@ -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
|
||||
@ -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'])
|
||||
@ -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"
|
||||
)
|
||||
@ -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}")
|
||||
@ -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>
|
||||
Loading…
x
Reference in New Issue
Block a user