From 624784b788a6dbba9547d04135a0515497deed6d Mon Sep 17 00:00:00 2001 From: igor_nadein Date: Wed, 21 Jan 2026 13:13:44 +0300 Subject: [PATCH] Remove outdated test files and fixtures --- backend/tests/TESTING_PLAN_LDAP_OPTIONAL.md | 916 ------------------ .../employees/test_position_permissions.py | 464 --------- backend/tests/feed/test_models.py | 180 ---- .../ldap/ldap_test_config/cn=config.ldif | 16 - .../cn=config/cn=module{0}.ldif | 16 - .../ldap_test_config/cn=config/cn=schema.ldif | 12 - .../cn=config/cn=schema/cn={0}core.ldif | 247 ----- .../cn=config/cn=schema/cn={1}cosine.ldif | 178 ---- .../cn=schema/cn={2}inetorgperson.ldif | 49 - .../cn=config/cn=schema/cn={3}rfc2307bis.ldif | 165 ---- .../cn=config/cn=schema/cn={4}ppolicy.ldif | 65 -- .../cn=config/cn=schema/cn={5}kopano.ldif | 175 ---- .../cn=schema/cn={6}openssh-lpk.ldif | 18 - .../cn=schema/cn={7}postfix-book.ldif | 50 - .../cn=config/cn=schema/cn={8}samba.ldif | 235 ----- .../cn=config/olcDatabase={-1}frontend.ldif | 18 - .../cn=config/olcDatabase={0}config.ldif | 16 - .../cn=config/olcDatabase={1}mdb.ldif | 32 - .../olcOverlay={0}memberof.ldif | 18 - .../olcOverlay={1}refint.ldif | 18 - .../docker-openldap-was-admin-password-set | 0 .../fixtures/ldap/ldap_test_data/data.mdb | Bin 118784 -> 0 bytes .../fixtures/ldap/ldap_test_data/lock.mdb | Bin 8192 -> 0 bytes backend/tests/integration/README.md | 229 ----- backend/tests/integration/README_NEW.md | 221 ----- backend/tests/integration/conftest.py | 177 ---- .../tests/integration/test_ldap_enabled.py | 182 ---- .../integration/test_ldap_integration.py.old | 213 ---- .../tests/integration/test_ldap_simple.py.old | 200 ---- .../ldap_fixtures/01-base-structure.ldif | 44 - backend/tests/manual/conftest.py | 8 - .../manual/test_announcement_notifications.py | 84 -- backend/tests/manual/test_avatar_simple.py | 168 ---- backend/tests/manual/test_corporate_ip.py | 52 - .../test_create_announcement_message.py | 81 -- backend/tests/manual/test_email.py | 114 --- backend/tests/manual/test_ip_restrictions.py | 171 ---- backend/tests/manual/test_ip_simple.py | 167 ---- backend/tests/manual/test_ldap_simple.py | 176 ---- .../tests/manual/test_notification_create.py | 43 - .../tests/manual/test_notification_debug.py | 95 -- .../tests/manual/test_notification_signals.py | 428 -------- backend/tests/manual/test_pagination.py | 106 -- backend/tests/manual/test_recipients.py | 310 ------ backend/tests/manual/test_recipients_api.py | 247 ----- backend/tests/manual/test_register_api.py | 148 --- backend/tests/manual/test_reply_update.py | 93 -- backend/tests/manual/test_requests_access.py | 278 ------ .../manual/test_requests_access_detailed.py | 289 ------ .../test_email_verification_middleware.py | 279 ------ backend/tests/notifications/__init__.py | 1 - backend/tests/notifications/test_api.py | 274 ------ backend/tests/notifications/test_models.py | 224 ----- backend/tests/notifications/test_services.py | 141 --- backend/tests/requests_app/conftest.py | 175 ---- .../tests/requests_app/test_filters_basic.py | 45 - backend/tests/requests_app/test_forms.py | 195 ---- .../tests/requests_app/test_list_requests.py | 312 ------ backend/tests/requests_app/test_my_and_all.py | 35 - backend/tests/requests_app/test_process.py | 63 -- .../requests_app/test_rate_limit_fallback.py | 24 - .../tests/requests_app/test_safe_back_url.py | 17 - backend/tests/requests_app_fixtures.py | 301 ------ .../tests/test_bidirectional_chat_loading.py | 503 ---------- backend/tests/test_static_files.py | 253 ----- backend/tests/test_template_components.py | 164 ---- backend/tests/textarea_autoexpand_test.html | 196 ---- 67 files changed, 10414 deletions(-) delete mode 100644 backend/tests/TESTING_PLAN_LDAP_OPTIONAL.md delete mode 100644 backend/tests/employees/test_position_permissions.py delete mode 100644 backend/tests/feed/test_models.py delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/cn=config.ldif delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=module{0}.ldif delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema.ldif delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={0}core.ldif delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={1}cosine.ldif delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={2}inetorgperson.ldif delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={3}rfc2307bis.ldif delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={4}ppolicy.ldif delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={5}kopano.ldif delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={6}openssh-lpk.ldif delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={7}postfix-book.ldif delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={8}samba.ldif delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={-1}frontend.ldif delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={0}config.ldif delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={1}mdb.ldif delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={1}mdb/olcOverlay={0}memberof.ldif delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={1}mdb/olcOverlay={1}refint.ldif delete mode 100644 backend/tests/fixtures/ldap/ldap_test_config/docker-openldap-was-admin-password-set delete mode 100644 backend/tests/fixtures/ldap/ldap_test_data/data.mdb delete mode 100644 backend/tests/fixtures/ldap/ldap_test_data/lock.mdb delete mode 100644 backend/tests/integration/README.md delete mode 100644 backend/tests/integration/README_NEW.md delete mode 100644 backend/tests/integration/conftest.py delete mode 100644 backend/tests/integration/test_ldap_enabled.py delete mode 100644 backend/tests/integration/test_ldap_integration.py.old delete mode 100644 backend/tests/integration/test_ldap_simple.py.old delete mode 100644 backend/tests/ldap_fixtures/01-base-structure.ldif delete mode 100644 backend/tests/manual/conftest.py delete mode 100644 backend/tests/manual/test_announcement_notifications.py delete mode 100644 backend/tests/manual/test_avatar_simple.py delete mode 100644 backend/tests/manual/test_corporate_ip.py delete mode 100644 backend/tests/manual/test_create_announcement_message.py delete mode 100644 backend/tests/manual/test_email.py delete mode 100644 backend/tests/manual/test_ip_restrictions.py delete mode 100644 backend/tests/manual/test_ip_simple.py delete mode 100644 backend/tests/manual/test_ldap_simple.py delete mode 100644 backend/tests/manual/test_notification_create.py delete mode 100644 backend/tests/manual/test_notification_debug.py delete mode 100644 backend/tests/manual/test_notification_signals.py delete mode 100644 backend/tests/manual/test_pagination.py delete mode 100644 backend/tests/manual/test_recipients.py delete mode 100644 backend/tests/manual/test_recipients_api.py delete mode 100644 backend/tests/manual/test_register_api.py delete mode 100644 backend/tests/manual/test_reply_update.py delete mode 100644 backend/tests/manual/test_requests_access.py delete mode 100644 backend/tests/manual/test_requests_access_detailed.py delete mode 100644 backend/tests/middleware/test_email_verification_middleware.py delete mode 100644 backend/tests/notifications/__init__.py delete mode 100644 backend/tests/notifications/test_api.py delete mode 100644 backend/tests/notifications/test_models.py delete mode 100644 backend/tests/notifications/test_services.py delete mode 100644 backend/tests/requests_app/conftest.py delete mode 100644 backend/tests/requests_app/test_filters_basic.py delete mode 100644 backend/tests/requests_app/test_forms.py delete mode 100644 backend/tests/requests_app/test_list_requests.py delete mode 100644 backend/tests/requests_app/test_my_and_all.py delete mode 100644 backend/tests/requests_app/test_process.py delete mode 100644 backend/tests/requests_app/test_rate_limit_fallback.py delete mode 100644 backend/tests/requests_app/test_safe_back_url.py delete mode 100644 backend/tests/requests_app_fixtures.py delete mode 100644 backend/tests/test_bidirectional_chat_loading.py delete mode 100644 backend/tests/test_static_files.py delete mode 100644 backend/tests/test_template_components.py delete mode 100644 backend/tests/textarea_autoexpand_test.html diff --git a/backend/tests/TESTING_PLAN_LDAP_OPTIONAL.md b/backend/tests/TESTING_PLAN_LDAP_OPTIONAL.md deleted file mode 100644 index 4f75a60e..00000000 --- a/backend/tests/TESTING_PLAN_LDAP_OPTIONAL.md +++ /dev/null @@ -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)
• Пароль установлен в LDAP
• Создана запись в БД (unusable_password)
• is_active=False
• Отправлено письмо верификации
• HTTP 201 | -| R2 | Регистрация с аватаром | + файл avatar | • Аватар загружен в LDAP
• Сохранён в БД
• HTTP 201 | -| R3 | Дублирование email | Email уже существует | HTTP 400 "email_exists" | -| R4 | Дублирование телефона | Телефон уже существует | HTTP 400 "phone_exists" | -| R5 | LDAP недоступен | LDAP сервер не отвечает | HTTP 502 "ldap_error" | -| R6 | Ошибка создания в БД | БД недоступна после LDAP | • LDAP пользователь удалён (rollback)
• HTTP 500 | - -#### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Тест-данные | Ожидаемый результат | -|----|----------|-------------|---------------------| -| R7 | Успешная регистрация | Валидные данные | • Создан пользователь только в БД
• Пароль установлен в БД (set_password)
• is_active=False
• Письмо верификации отправлено
• HTTP 201 | -| R8 | Регистрация с аватаром | + файл avatar | • Аватар сохранён в media/
• HTTP 201 | -| R9 | Дублирование данных | Email/телефон существует | HTTP 400 | - -**Проверки после регистрации:** -```python -# С LDAP -assert Employee.objects.filter(email=test_email).exists() -assert not user.has_usable_password() # Пароль в LDAP -assert LdapSyncState.objects.filter(model='employee', object_pk=user.id).exists() - -# Без LDAP -assert Employee.objects.filter(email=test_email).exists() -assert user.has_usable_password() # Пароль в БД -assert user.check_password(test_password) -``` - ---- - -## 3. Тестирование VerifyEmailAPIView - -### 3.1 Верификация email (POST /api/v1/verify-email/) - -#### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Предусловия | Ожидаемый результат | -|----|----------|-------------|---------------------| -| V1 | Успешная верификация | Пользователь существует, не верифицирован | • is_active=True в БД
• Активирован в LDAP (enabled)
• HTTP 200 | -| V2 | Повторная верификация | Уже верифицирован | HTTP 400 "already_verified" | -| V3 | Неверный токен | Токен не существует/истёк | HTTP 404 "user_not_found" | -| V4 | LDAP ошибка активации | LDAP недоступен | HTTP 502 с деталями ошибки | - -#### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Предусловия | Ожидаемый результат | -|----|----------|-------------|---------------------| -| V5 | Успешная верификация | Пользователь не верифицирован | • is_active=True в БД
• HTTP 200 | -| V6 | Повторная верификация | Уже верифицирован | HTTP 400 | - -**Проверки после верификации:** -```python -# С LDAP -assert user.is_active -assert ldap_user_is_enabled(user.ldap_dn) - -# Без LDAP -assert user.is_active -assert user.check_password(original_password) # Пароль сохранён -``` - ---- - -## 4. Тестирование DepartmentViewSet - -### 4.1 Создание отдела (POST /api/v1/departments/) - -#### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Тест-данные | Ожидаемый результат | -|----|----------|-------------|---------------------| -| D1 | Создание отдела | name, description | • OU создан в LDAP
• Запись в БД
• HTTP 201 | -| D2 | Создание с главой | + head_id | • OU создан
• head установлен через DirectoryService
• HTTP 201 | -| D3 | LDAP ошибка | LDAP недоступен | HTTP 502 | -| D4 | Ошибка БД после LDAP | БД недоступна | • OU удалён (rollback)
• HTTP 500 | - -#### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Тест-данные | Ожидаемый результат | -|----|----------|-------------|---------------------| -| D5 | Создание отдела | name, description | • Только запись в БД
• HTTP 201 | -| D6 | Создание с главой | + head_id | • Запись в БД с head
• HTTP 201 | - -### 4.2 Обновление отдела (PATCH /api/v1/departments/{id}/) - -#### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Изменения | Ожидаемый результат | -|----|----------|-----------|---------------------| -| D7 | Изменение имени | name="New Name" | • OU переименован в LDAP
• Обновлено в БД
• HTTP 200 | -| D8 | Смена главы | head_id=new_head | • set_head() в LDAP
• head_appointed_at установлен
• HTTP 200 | -| D9 | Удаление главы | head_id=null | • set_head(None) в LDAP
• head=null в БД
• HTTP 200 | - -#### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Изменения | Ожидаемый результат | -|----|----------|-----------|---------------------| -| D10 | Изменение имени | name="New Name" | • Обновлено только в БД
• HTTP 200 | -| D11 | Смена главы | head_id=new_head | • head обновлён в БД
• head_appointed_at установлен
• HTTP 200 | - -### 4.3 Управление участниками отдела - -#### add_member (POST /api/v1/departments/{id}/add-member/) - -##### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| D12 | Добавление участника | employee_id | • Пользователь MOVE в OU отдела в LDAP
• DepartmentMember создан/активирован
• HTTP 200 | -| D13 | Повторное добавление | Уже участник | • Активация существующей связи
• HTTP 200 | -| D14 | LDAP ошибка | LDAP недоступен | HTTP 502 | - -##### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| D15 | Добавление участника | employee_id | • DepartmentMember создан/активирован в БД
• HTTP 200 | - -#### remove_member (POST /api/v1/departments/{id}/remove-member/) - -##### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| D16 | Удаление участника | employee_id | • Пользователь MOVE обратно в Users OU
• is_active=False в DepartmentMember
• HTTP 200 | -| D17 | Удаление несуществующего | Нет такого участника | HTTP 404 | - -##### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| D18 | Удаление участника | employee_id | • is_active=False в БД
• HTTP 200 | - -### 4.4 set_member_role (POST /api/v1/departments/{id}/set-member-role/) - -#### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| D19 | Назначение роли | employee_id, role_id | • Роль назначена в LDAP (группы)
• role обновлена в БД
• HTTP 200 | -| D20 | Снятие роли | employee_id, role_id=null | • Роли удалены в LDAP
• role=null в БД
• HTTP 200 | - -#### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| D21 | Назначение роли | employee_id, role_id | • role обновлена только в БД
• HTTP 200 | - -### 4.5 Удаление отдела (DELETE /api/v1/departments/{id}/) - -#### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Условия | Ожидаемый результат | -|----|----------|---------|---------------------| -| D22 | Удаление пустого отдела | Нет участников | • OU удалён в LDAP
• Удалено из БД
• HTTP 204 | -| D23 | Удаление с участниками | Есть активные участники | • OU удалён
• Участники перемещены в Users OU
• HTTP 204 | -| D24 | LDAP ошибка без force_db | LDAP недоступен, force_db=false | HTTP 502 | -| D25 | LDAP ошибка с force_db | LDAP недоступен, force_db=true | • БД удалена несмотря на LDAP
• HTTP 204 | - -#### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Условия | Ожидаемый результат | -|----|----------|---------|---------------------| -| D26 | Удаление отдела | Любые условия | • Удалено из БД
• HTTP 204 | - ---- - -## 5. Тестирование EmployeeViewSet - -### 5.1 Создание сотрудника (POST /api/v1/employees/) - -#### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| E1 | Создание базовое | email, имя, пароль | • Пользователь в LDAP
• Запись в БД
• HTTP 201 | -| E2 | Создание с позицией | + position_id | • Создан в LDAP
• assign_position() вызван
• Группы позиции назначены
• HTTP 201 | -| E3 | Создание с аватаром | + avatar file | • Аватар в LDAP
• HTTP 201 | -| E4 | LDAP ошибка | LDAP недоступен | HTTP 502 | - -#### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| E5 | Создание базовое | email, имя, пароль | • Только в БД
• Пароль через set_password()
• HTTP 201 | -| E6 | Создание с позицией | + position_id | • Создан в БД
• position установлена
• Группы через M2M
• HTTP 201 | - -### 5.2 Обновление сотрудника (PATCH /api/v1/employees/{id}/) - -#### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Изменения | Ожидаемый результат | -|----|----------|-----------|---------------------| -| E7 | Обновление имени | first_name, last_name | • update_user() в LDAP
• Обновлено в БД
• HTTP 200 | -| E8 | Смена email | email="new@mail.com" | • Email обновлён в LDAP
• Обновлён в БД
• HTTP 200 | -| E9 | Смена позиции | position_id=new_position | • Позиция обновлена в LDAP
• Группы пересчитаны
• HTTP 200 | -| E10 | Удаление позиции | position=null | • Позиция удалена в LDAP
• position=null в БД
• HTTP 200 | -| E11 | Обновление аватара | + новый avatar | • Аватар обновлён в LDAP
• Сохранён в БД
• HTTP 200 | -| E12 | Перемещение в отдел | department_dn=dept_dn | • MOVE в LDAP
• HTTP 200 | -| E13 | Назначение групп | group_cns=['CN=Group1',...] | • Группы назначены в LDAP
• HTTP 200 | - -#### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Изменения | Ожидаемый результат | -|----|----------|-----------|---------------------| -| E14 | Обновление всех полей | first_name, last_name, email, phone, position, avatar | • Все обновлено в БД
• Аватар сохранён в media/
• HTTP 200 | -| E15 | Смена позиции | position_id=new_position | • Позиция обновлена в БД
• Группы через M2M
• HTTP 200 | - -**Проверки:** -```python -# С LDAP -user = Employee.objects.get(id=test_id) -ldap_user = DirectoryService().get_user_by_dn(user.ldap_dn) -assert ldap_user['givenName'] == user.first_name -assert ldap_user['sn'] == user.last_name - -# Без LDAP -user = Employee.objects.get(id=test_id) -assert user.first_name == expected_first_name -assert user.position_id == expected_position_id -``` - ---- - -## 6. Тестирование PositionViewSet - -### 6.1 CRUD позиций - -#### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Действие | Ожидаемый результат | -|----|----------|----------|---------------------| -| P1 | Создание позиции | POST /api/v1/positions/ | • Позиция в БД
• reconcile_position() вызван
• Группа создана в LDAP
• HTTP 201 | -| P2 | Обновление позиции | PATCH /api/v1/positions/{id}/ | • Обновлено в БД
• reconcile_position() вызван
• HTTP 200 | -| P3 | Удаление позиции | DELETE /api/v1/positions/{id}/ | • delete_position_group() вызван
• Удалено из БД
• HTTP 204 | -| P4 | Назначение групп | POST /positions/{id}/set-groups | • groups.set() в БД
• reconcile_position() синхронизирует LDAP
• HTTP 200 | -| P5 | Добавление групп | POST /positions/{id}/add-groups | • groups.add() в БД
• reconcile_position()
• HTTP 200 | -| P6 | Удаление групп | POST /positions/{id}/remove-groups | • groups.remove() в БД
• reconcile_position()
• HTTP 200 | - -#### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Действие | Ожидаемый результат | -|----|----------|----------|---------------------| -| P7 | Создание позиции | POST /api/v1/positions/ | • Только в БД
• LDAP не вызывается
• HTTP 201 | -| P8 | Обновление позиции | PATCH /api/v1/positions/{id}/ | • Обновлено в БД
• HTTP 200 | -| P9 | Удаление позиции | DELETE /api/v1/positions/{id}/ | • Удалено из БД
• HTTP 204 | -| P10 | Управление группами | set/add/remove-groups | • Только M2M в БД
• HTTP 200 | - -**Проверки _ldap_try():** -```python -# _ldap_try() должен возвращать None в обоих случаях: -# - LDAP_ENABLED=True и операция успешна → None -# - LDAP_ENABLED=False → None (операция пропущена) - -# Это не должно блокировать дальнейшее выполнение -``` - ---- - -## 7. Тестирование GroupViewSet - -### 7.1 Создание группы (POST /api/v1/groups/) - -#### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| G1 | Создание группы | name, description | • Группа создана в LDAP
• Запись в БД
• HTTP 201 | -| G2 | С permissions | + permissions=[1,2,3] | • Группа в LDAP
• Permissions в БД
• HTTP 201 | -| G3 | LDAP ошибка | LDAP недоступен | HTTP 502 | -| G4 | Ошибка БД | БД сбой после LDAP | • Группа удалена из LDAP (rollback)
• HTTP 500 | - -#### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| G5 | Создание группы | name, description, permissions | • Только в БД
• Permissions через M2M
• HTTP 201 | - -### 7.2 Обновление группы (PATCH /api/v1/groups/{id}/) - -#### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Изменения | Ожидаемый результат | -|----|----------|-----------|---------------------| -| G6 | Переименование | name="New Name" | • group_rename() в LDAP
• Обновлено в БД
• HTTP 200 | -| G7 | Изменение описания | ldap_description="..." | • group_set_description() в LDAP
• HTTP 200 | -| G8 | Группа не найдена в LDAP | _resolve_group_dn() → None | HTTP 404 | - -#### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Изменения | Ожидаемый результат | -|----|----------|-----------|---------------------| -| G9 | Переименование | name="New Name" | • Обновлено в БД
• HTTP 200 | - -### 7.3 Action методы группы - -#### rename (POST /api/v1/groups/{id}/rename/) - -##### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| G10 | Переименование | new_name="NewName" | • group_rename() в LDAP
• name обновлён в БД
• HTTP 200 | -| G11 | LDAP ошибка | LDAP недоступен | HTTP 502 | - -##### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| G12 | Переименование | new_name="NewName" | • Обновлено в БД
• HTTP 200 | - -#### set_description (POST /api/v1/groups/{id}/set-description/) - -##### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| G13 | Установка описания | description="..." | • group_set_description() в LDAP
• HTTP 200 | - -##### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| G14 | Установка описания | description="..." | • Ничего не происходит (нет поля в БД)
• HTTP 200 | - -#### members (GET /api/v1/groups/{id}/members/) - -##### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Условия | Ожидаемый результат | -|----|----------|---------|---------------------| -| G15 | Получение участников | Группа существует | • group_list_members() из LDAP
• Сопоставление с БД через LdapSyncState
• HTTP 200 {"dns": [...], "employees": [...]} | - -##### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Условия | Ожидаемый результат | -|----|----------|---------|---------------------| -| G16 | Получение участников | Группа существует | • Участники из БД (grp.user_set.all())
• HTTP 200 {"dns": [], "employees": [...]} | - -#### add_members (POST /api/v1/groups/{id}/add-members/) - -##### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| G17 | Добавление через DN | member_dns=['CN=User1,...'] | • group_add_members() в LDAP
• user_set.add() в БД
• HTTP 200 | -| G18 | Добавление через ID | member_ids=[1,2,3] | • ID→DN преобразование
• Добавлены в LDAP и БД
• HTTP 200 | -| G19 | Ошибка БД | БД сбой после LDAP | • group_remove_members() (rollback)
• HTTP 500 | - -##### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| G20 | Добавление участников | member_ids=[1,2,3] | • user_set.add() в БД
• HTTP 200 | - -#### remove_members (POST /api/v1/groups/{id}/remove-members/) - -##### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| G21 | Удаление участников | member_dns/member_ids | • group_remove_members() в LDAP
• user_set.remove() в БД
• HTTP 200 | -| G22 | Ошибка БД | БД сбой после LDAP | • group_add_members() (rollback)
• HTTP 500 | - -##### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| G23 | Удаление участников | member_ids=[1,2,3] | • user_set.remove() в БД
• HTTP 200 | - -#### replace_members (POST /api/v1/groups/{id}/replace-members/) - -##### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| G24 | Замена состава | member_dns/member_ids | • Сохранён старый состав
• group_replace_members() в LDAP
• user_set.set() в БД
• HTTP 200 | -| G25 | Ошибка БД | БД сбой после LDAP | • group_replace_members(prev_dns) (rollback)
• HTTP 500 | - -##### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| G26 | Замена состава | member_ids=[...] | • user_set.set() в БД
• HTTP 200 | - -### 7.4 Удаление группы (DELETE /api/v1/groups/{id}/) - -#### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Параметры | Ожидаемый результат | -|----|----------|-----------|---------------------| -| G27 | Удаление группы | force_db=false | • group_delete() в LDAP
• Удалено из БД
• HTTP 204 | -| G28 | LDAP ошибка без force | force_db=false, LDAP недоступен | HTTP 502 | -| G29 | LDAP ошибка с force | force_db=true, LDAP недоступен | • Игнорируем LDAP
• Удалено из БД
• HTTP 204 | - -#### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Параметры | Ожидаемый результат | -|----|----------|-----------|---------------------| -| G30 | Удаление группы | Любые | • Удалено из БД
• HTTP 204 | - -### 7.5 Список групп (GET /api/v1/groups/) - -#### С LDAP (LDAP_ENABLED=True) -| ID | Сценарий | Условия | Ожидаемый результат | -|----|----------|---------|---------------------| -| G31 | Получение списка | Любые | • sync_groups_catalog() вызван (throttled)
• Список из БД
• HTTP 200 | - -#### Без LDAP (LDAP_ENABLED=False) -| ID | Сценарий | Условия | Ожидаемый результат | -|----|----------|---------|---------------------| -| G32 | Получение списка | Любые | • Список из БД
• HTTP 200 | - ---- - -## 8. Интеграционные тесты - -### 8.1 Сквозной сценарий: Регистрация → Верификация → Назначение позиции - -#### С LDAP -| Шаг | Действие | Проверка | -|-----|----------|----------| -| 1 | POST /api/v1/register/ | Пользователь в LDAP (disabled) и БД (inactive) | -| 2 | POST /api/v1/verify-email/ | Активирован в LDAP и БД | -| 3 | PATCH /api/v1/employees/{id}/ (position_id) | Группы позиции назначены в LDAP | -| 4 | GET /api/v1/employees/{id}/ | position корректна, is_active=true | - -#### Без LDAP -| Шаг | Действие | Проверка | -|-----|----------|----------| -| 1 | POST /api/v1/register/ | Пользователь только в БД, пароль установлен | -| 2 | POST /api/v1/verify-email/ | is_active=true в БД | -| 3 | PATCH /api/v1/employees/{id}/ (position_id) | Позиция обновлена, группы через M2M | -| 4 | Аутентификация | EmailOrPhoneBackend работает, пароль проверяется из БД | - -### 8.2 Сквозной сценарий: Создание отдела → Добавление участника → Назначение роли - -#### С LDAP -| Шаг | Действие | Проверка | -|-----|----------|----------| -| 1 | POST /api/v1/departments/ | OU создан в LDAP | -| 2 | POST /departments/{id}/add-member/ | Пользователь перемещён в OU | -| 3 | POST /departments/{id}/set-member-role/ | Роли назначены через LDAP группы | - -#### Без LDAP -| Шаг | Действие | Проверка | -|-----|----------|----------| -| 1 | POST /api/v1/departments/ | Запись в БД | -| 2 | POST /departments/{id}/add-member/ | DepartmentMember создан в БД | -| 3 | POST /departments/{id}/set-member-role/ | role обновлена в БД | - -### 8.3 Сценарий: Создание группы → Добавление участников → Проверка permissions - -#### С LDAP -| Шаг | Действие | Проверка | -|-----|----------|----------| -| 1 | POST /api/v1/groups/ (+ permissions) | Группа в LDAP, permissions в БД | -| 2 | POST /groups/{id}/add-members/ | Участники добавлены в LDAP группу и БД | -| 3 | Проверка прав пользователя | user.has_perm() работает корректно | - -#### Без LDAP -| Шаг | Действие | Проверка | -|-----|----------|----------| -| 1 | POST /api/v1/groups/ (+ permissions) | Группа и permissions в БД | -| 2 | POST /groups/{id}/add-members/ | user_set.add() в БД | -| 3 | Проверка прав | user.has_perm() работает через Django permissions | - ---- - -## 9. Тесты граничных условий и ошибок - -### 9.1 Переключение режима во время работы - -| ID | Сценарий | Действия | Ожидаемый результат | -|----|----------|----------|---------------------| -| B1 | LDAP→No LDAP | 1. Создать пользователя с LDAP
2. Отключить LDAP
3. Обновить пользователя | • Обновление работает только с БД
• Данные не теряются | -| B2 | No LDAP→LDAP | 1. Создать пользователя без LDAP
2. Включить LDAP
3. Обновить пользователя | • Обновление пытается синхронизировать с LDAP
• Может вернуть 502 если пользователя нет в LDAP | - -### 9.2 Частичные сбои LDAP - -| ID | Сценарий | Условия | Ожидаемый результат | -|----|----------|---------|---------------------| -| B3 | LDAP timeout | LDAP_ENABLED=True, LDAP не отвечает | HTTP 502 с деталями ошибки | -| B4 | LDAP частичный сбой | Создание в LDAP OK, обновление FAIL | Зависит от операции - либо rollback, либо 502 | -| B5 | БД сбой после LDAP | LDAP OK, БД FAIL | Rollback LDAP операции, HTTP 500 | - -### 9.3 Некорректные данные - -| ID | Сценарий | Данные | Ожидаемый результат | -|----|----------|--------|---------------------| -| B6 | LDAP_ENABLED не boolean | "yes"/"no"/"1"/"0" | Корректно обрабатывается через .lower() == "true" | -| B7 | Отсутствующие LDAP настройки | LDAP_ENABLED=True, но нет SERVER_URI | Ошибка при инициализации DirectoryService | -| B8 | Некорректный DN | Попытка работы с несуществующим DN | HTTP 404 или 502 в зависимости от метода | - ---- - -## 10. Тесты производительности - -### 10.1 Массовые операции - -| ID | Сценарий | Параметры | Проверка | -|----|----------|-----------|----------| -| P1 | Массовая регистрация | 100 пользователей | • С LDAP: время создания < 30s
• Без LDAP: время < 10s | -| P2 | Добавление 50 участников в группу | 50 member_ids | • Операция выполняется атомарно
• При ошибке - полный rollback | -| P3 | Создание 20 отделов | 20 departments | • С LDAP: все OU созданы
• Без LDAP: только БД | - -### 10.2 Нагрузочное тестирование - -| ID | Сценарий | Условия | Цель | -|----|----------|---------|------| -| P4 | Concurrent создание пользователей | 10 параллельных запросов | Нет race conditions | -| P5 | Обновление одного пользователя | 5 параллельных PATCH | Последовательность транзакций корректна | - ---- - -## 11. Автоматизация тестирования - -### 11.1 Структура тестов - -``` -tests/ -├── test_ldap_optional/ -│ ├── __init__.py -│ ├── conftest.py # Фикстуры для двух режимов -│ ├── test_helpers.py # Тесты вспомогательных функций -│ ├── test_register.py # RegisterAPIView -│ ├── test_verify_email.py # VerifyEmailAPIView -│ ├── test_departments.py # DepartmentViewSet -│ ├── test_employees.py # EmployeeViewSet -│ ├── test_positions.py # PositionViewSet -│ ├── test_groups.py # GroupViewSet -│ ├── test_integration.py # Сквозные сценарии -│ └── test_edge_cases.py # Граничные условия -``` - -### 11.2 Фикстуры pytest - -```python -# conftest.py -import pytest -from django.conf import settings - -@pytest.fixture(params=[True, False], ids=["with_ldap", "without_ldap"]) -def ldap_mode(request, settings): - """Параметризованная фикстура для тестирования обоих режимов""" - original_value = getattr(settings, 'LDAP_ENABLED', False) - settings.LDAP_ENABLED = request.param - yield request.param - settings.LDAP_ENABLED = original_value - -@pytest.fixture -def with_ldap(settings): - """Фикстура для тестов только с LDAP""" - settings.LDAP_ENABLED = True - yield - settings.LDAP_ENABLED = False - -@pytest.fixture -def without_ldap(settings): - """Фикстура для тестов только без LDAP""" - settings.LDAP_ENABLED = False - yield - -@pytest.fixture -def test_user_data(): - """Базовые данные для создания тестового пользователя""" - return { - "email": "test@example.com", - "first_name": "Test", - "last_name": "User", - "phone_number": "+79001234567", - "password": "SecurePass123!", - "whatsapp": "+79001234567" - } -``` - -### 11.3 Примеры тестов - -```python -# test_register.py -import pytest -from rest_framework.test import APIClient -from employees.models import Employee - -@pytest.mark.django_db -class TestRegisterAPIView: - - def test_register_user_both_modes(self, ldap_mode, test_user_data): - """R1, R7: Регистрация в обоих режимах""" - client = APIClient() - response = client.post('/api/v1/register/', test_user_data) - - assert response.status_code == 201 - user = Employee.objects.get(email=test_user_data['email']) - assert user.is_active is False - - if ldap_mode: # LDAP_ENABLED=True - assert not user.has_usable_password() # Пароль в LDAP - # Проверка создания в LDAP - from employees.ldap.services import DirectoryService - svc = DirectoryService() - ldap_user = svc.get_user_by_email(test_user_data['email']) - assert ldap_user is not None - else: # LDAP_ENABLED=False - assert user.has_usable_password() # Пароль в БД - assert user.check_password(test_user_data['password']) - - def test_register_with_avatar_with_ldap(self, with_ldap, test_user_data): - """R2: Регистрация с аватаром (LDAP)""" - client = APIClient() - with open('test_files/avatar.jpg', 'rb') as avatar: - test_user_data['avatar'] = avatar - response = client.post('/api/v1/register/', test_user_data, format='multipart') - - assert response.status_code == 201 - user = Employee.objects.get(email=test_user_data['email']) - assert user.avatar # Аватар сохранён - - def test_register_duplicate_email(self, ldap_mode, test_user_data): - """R3, R9: Дублирование email""" - client = APIClient() - Employee.objects.create_user(**test_user_data) - - response = client.post('/api/v1/register/', test_user_data) - assert response.status_code == 400 -``` - -```python -# test_groups.py -import pytest -from rest_framework.test import APIClient -from django.contrib.auth.models import Group - -@pytest.mark.django_db -class TestGroupViewSet: - - def test_create_group_both_modes(self, ldap_mode, admin_user): - """G1, G5: Создание группы""" - client = APIClient() - client.force_authenticate(user=admin_user) - - data = {"name": "TestGroup", "ldap_description": "Test description"} - response = client.post('/api/v1/groups/', data) - - assert response.status_code == 201 - group = Group.objects.get(name="TestGroup") - assert group is not None - - if ldap_mode: - # Проверка создания в LDAP - from employees.ldap.services import DirectoryService - svc = DirectoryService() - dn = svc.group_find_dn("TestGroup") - assert dn is not None - - def test_add_members_both_modes(self, ldap_mode, admin_user, test_group, test_users): - """G17, G20: Добавление участников""" - client = APIClient() - client.force_authenticate(user=admin_user) - - member_ids = [u.id for u in test_users[:3]] - response = client.post( - f'/api/v1/groups/{test_group.id}/add-members/', - {"member_ids": member_ids} - ) - - assert response.status_code == 200 - assert test_group.user_set.count() == 3 - - if ldap_mode: - # Проверка в LDAP - from employees.ldap.services import DirectoryService - svc = DirectoryService() - dn = svc.group_find_dn(test_group.name) - members = svc.group_list_members(dn) - assert len(members) == 3 -``` - -### 11.4 Команды для запуска тестов - -```bash -# Все тесты в обоих режимах -pytest tests/test_ldap_optional/ -v - -# Только тесты с LDAP -pytest tests/test_ldap_optional/ -v -k "with_ldap" - -# Только тесты без LDAP -pytest tests/test_ldap_optional/ -v -k "without_ldap" - -# Конкретный тестовый файл -pytest tests/test_ldap_optional/test_register.py -v - -# С покрытием кода -pytest tests/test_ldap_optional/ --cov=api.v1.employees.views --cov-report=html - -# Параллельный запуск (быстрее) -pytest tests/test_ldap_optional/ -v -n auto -``` - ---- - -## 12. Ручное тестирование - -### 12.1 Чек-лист для ручной проверки - -#### Подготовка -- [ ] Создать .env файл с LDAP_ENABLED=true -- [ ] Создать .env файл с LDAP_ENABLED=false -- [ ] Поднять LDAP сервер (для режима с LDAP) -- [ ] Запустить миграции БД - -#### Регистрация и верификация -- [ ] Зарегистрировать пользователя с LDAP -- [ ] Зарегистрировать пользователя без LDAP -- [ ] Верифицировать email в обоих режимах -- [ ] Попытаться войти после верификации - -#### Отделы -- [ ] Создать отдел с LDAP -- [ ] Создать отдел без LDAP -- [ ] Добавить участника в отдел (оба режима) -- [ ] Удалить участника из отдела (оба режима) -- [ ] Назначить роль участнику (оба режима) -- [ ] Удалить отдел (оба режима) - -#### Сотрудники -- [ ] Создать сотрудника через админ-панель (оба режима) -- [ ] Обновить имя/email/телефон (оба режима) -- [ ] Назначить позицию (оба режима) -- [ ] Загрузить аватар (оба режима) - -#### Позиции -- [ ] Создать позицию (оба режима) -- [ ] Назначить группы позиции (оба режима) -- [ ] Удалить позицию (оба режима) - -#### Группы -- [ ] Создать группу (оба режима) -- [ ] Переименовать группу (оба режима) -- [ ] Добавить участников в группу (оба режима) -- [ ] Удалить участников из группы (оба режима) -- [ ] Удалить группу (оба режима) - -### 12.2 Инструменты для ручного тестирования - -**Postman Collection:** -- Импортировать коллекцию API endpoints -- Создать окружения для LDAP/No-LDAP -- Выполнить последовательность запросов - -**LDAP Browser (для режима с LDAP):** -- Apache Directory Studio -- JXplorer -- Проверка структуры OU, пользователей, групп - -**Django Admin:** -- Проверка записей в БД -- Сравнение с LDAP (в режиме с LDAP) - ---- - -## 13. Критерии успешности тестирования - -### 13.1 Обязательные критерии (PASS/FAIL) - -✅ **PASS критерии:** -1. Все автоматические тесты проходят в обоих режимах (≥95% успешности) -2. Нет критических ошибок (500) при корректных запросах -3. Данные в БД консистентны после всех операций -4. В режиме с LDAP: данные синхронизированы между LDAP и БД -5. В режиме без LDAP: все операции работают без вызовов DirectoryService -6. Rollback механизмы работают корректно при ошибках -7. Аутентификация работает в обоих режимах - -### 13.2 Метрики качества - -| Метрика | Целевое значение | -|---------|------------------| -| Покрытие кода тестами | ≥ 90% | -| Успешность автотестов | 100% | -| Время выполнения всех тестов | < 5 минут | -| Количество критических багов | 0 | -| Количество major багов | ≤ 2 | -| Производительность (регистрация с LDAP) | < 3s на пользователя | -| Производительность (регистрация без LDAP) | < 1s на пользователя | - -### 13.3 Тест-репорт - -После завершения тестирования создать отчёт: - -```markdown -# Отчёт о тестировании LDAP Optional Integration - -## Окружение -- Django версия: X.X.X -- Python версия: X.X.X -- LDAP сервер: OpenLDAP X.X.X / AD - -## Результаты автотестов -- Всего тестов: XXX -- Успешно: XXX (XX%) -- Провалено: XX (XX%) -- Пропущено: XX - -## Покрытие кода -- Общее: XX% -- views.py: XX% - -## Найденные баги -1. [CRITICAL] Описание бага... -2. [MAJOR] Описание бага... - -## Рекомендации -- Улучшение 1... -- Улучшение 2... - -## Заключение -✅ Система готова к развёртыванию / ❌ Требуется доработка -``` - ---- - -## 14. Continuous Integration (CI) - -### 14.1 GitHub Actions / GitLab CI - -```yaml -# .github/workflows/test_ldap_optional.yml -name: Test LDAP Optional Integration - -on: [push, pull_request] - -jobs: - test-with-ldap: - runs-on: ubuntu-latest - services: - ldap: - image: osixia/openldap:latest - ports: - - 389:389 - env: - LDAP_ORGANISATION: "Test Org" - LDAP_DOMAIN: "example.com" - LDAP_ADMIN_PASSWORD: "admin" - - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.10' - - name: Install dependencies - run: | - pip install -r requirements.txt - - name: Run tests with LDAP - env: - LDAP_ENABLED: true - LDAP_SERVER_URI: ldap://localhost:389 - run: | - pytest tests/test_ldap_optional/ -v -k "with_ldap" - - test-without-ldap: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.10' - - name: Install dependencies - run: | - pip install -r requirements.txt - - name: Run tests without LDAP - env: - LDAP_ENABLED: false - run: | - pytest tests/test_ldap_optional/ -v -k "without_ldap" -``` - ---- - -## 15. Заключение - -Этот план тестирования обеспечивает: - -1. **Полноту покрытия** - все 50+ методов во всех ViewSets -2. **Двойное тестирование** - каждая функция в обоих режимах -3. **Автоматизацию** - pytest тесты для CI/CD -4. **Документированность** - четкие test case ID и ожидаемые результаты -5. **Воспроизводимость** - фикстуры и примеры кода -6. **Масштабируемость** - легко добавить новые тест-кейсы - -**Следующие шаги:** -1. Реализовать базовые автотесты (conftest.py, test_helpers.py) -2. Постепенно покрыть каждый ViewSet тестами -3. Настроить CI для автоматического прогона -4. Провести ручное тестирование по чек-листу -5. Исправить найденные баги -6. Создать итоговый отчёт diff --git a/backend/tests/employees/test_position_permissions.py b/backend/tests/employees/test_position_permissions.py deleted file mode 100644 index 26044640..00000000 --- a/backend/tests/employees/test_position_permissions.py +++ /dev/null @@ -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") diff --git a/backend/tests/feed/test_models.py b/backend/tests/feed/test_models.py deleted file mode 100644 index ca91922e..00000000 --- a/backend/tests/feed/test_models.py +++ /dev/null @@ -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] diff --git a/backend/tests/fixtures/ldap/ldap_test_config/cn=config.ldif b/backend/tests/fixtures/ldap/ldap_test_config/cn=config.ldif deleted file mode 100644 index f4661019..00000000 --- a/backend/tests/fixtures/ldap/ldap_test_config/cn=config.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=module{0}.ldif b/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=module{0}.ldif deleted file mode 100644 index 8ab8bf34..00000000 --- a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=module{0}.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema.ldif b/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema.ldif deleted file mode 100644 index 1c47d552..00000000 --- a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={0}core.ldif b/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={0}core.ldif deleted file mode 100644 index f1d086f6..00000000 --- a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={0}core.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={1}cosine.ldif b/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={1}cosine.ldif deleted file mode 100644 index d1d56524..00000000 --- a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={1}cosine.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={2}inetorgperson.ldif b/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={2}inetorgperson.ldif deleted file mode 100644 index 1e0c7ecd..00000000 --- a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={2}inetorgperson.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={3}rfc2307bis.ldif b/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={3}rfc2307bis.ldif deleted file mode 100644 index 4ac41a0e..00000000 --- a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={3}rfc2307bis.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={4}ppolicy.ldif b/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={4}ppolicy.ldif deleted file mode 100644 index c3f04ab3..00000000 --- a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={4}ppolicy.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={5}kopano.ldif b/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={5}kopano.ldif deleted file mode 100644 index 71515f80..00000000 --- a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={5}kopano.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={6}openssh-lpk.ldif b/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={6}openssh-lpk.ldif deleted file mode 100644 index 97ef9e72..00000000 --- a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={6}openssh-lpk.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={7}postfix-book.ldif b/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={7}postfix-book.ldif deleted file mode 100644 index 0a434561..00000000 --- a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={7}postfix-book.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={8}samba.ldif b/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={8}samba.ldif deleted file mode 100644 index b1cb0d46..00000000 --- a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/cn=schema/cn={8}samba.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={-1}frontend.ldif b/backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={-1}frontend.ldif deleted file mode 100644 index 7617214c..00000000 --- a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={-1}frontend.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={0}config.ldif b/backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={0}config.ldif deleted file mode 100644 index 8cdef5da..00000000 --- a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={0}config.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={1}mdb.ldif b/backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={1}mdb.ldif deleted file mode 100644 index d49fe41d..00000000 --- a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={1}mdb.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={1}mdb/olcOverlay={0}memberof.ldif b/backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={1}mdb/olcOverlay={0}memberof.ldif deleted file mode 100644 index c2ae9afc..00000000 --- a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={1}mdb/olcOverlay={0}memberof.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={1}mdb/olcOverlay={1}refint.ldif b/backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={1}mdb/olcOverlay={1}refint.ldif deleted file mode 100644 index 96283ef8..00000000 --- a/backend/tests/fixtures/ldap/ldap_test_config/cn=config/olcDatabase={1}mdb/olcOverlay={1}refint.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/fixtures/ldap/ldap_test_config/docker-openldap-was-admin-password-set b/backend/tests/fixtures/ldap/ldap_test_config/docker-openldap-was-admin-password-set deleted file mode 100644 index e69de29b..00000000 diff --git a/backend/tests/fixtures/ldap/ldap_test_data/data.mdb b/backend/tests/fixtures/ldap/ldap_test_data/data.mdb deleted file mode 100644 index 009ffc68d8fa358c5b778585d71b17c0c49ffc9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 118784 zcmeHQU2Ig>wcePy$@t=|oj z97vl+ZXYThP=ul=if(9C_2CNTr_EE45^j`M(Uq#8t332dX%cbUK15N1^7ByLL@MI0 zZ|}3t&v^VZ2E!QEwzl^^`+vT(_Bm(mz1G@Fv5V`-r8ho(eFZ0(aJvpxD-~BkP7pJN zx=f(fd!Y$}QbEk*SYEm2djA@!FBgJ%;$lJu;}E^&n;nF#q4}mFIeV9ce}`1OY)n5D)|e0YN|z5CjAPK|l}?1Ox#=;F=KN z{dc9hRIfVI^QQgZ)+xIuu`gj+`|LA4PbMz*Tuj96r>qfX*{`WTsVoQx0)l`bAP5Ko zf`A|(2nYg#03(p1NSBJK38y@cwA>sr$GoR#dzDSyxSDV$3T|z9%+&WX=LoKPj_bRX zM(wek2lku#+=~j0#58uN*XA8wXzF(>U4L|R&#r|w@Go8i%CvBY_X|vQ&YL}qj=$Th zJz6fc+kn@9w>nxb?VkdOi*8}+8e3SYs0Gve>^FdOe9qM1*QJV;b_Z^1u2R*)aktpm zIquZ!^Y#8!6d)Hff%4Fcsd6dfA}vmo%tH+LY*U(P=PO=i(|=5rDw#5pdi5l1r zWm8?JWa`c(r@z*#^EEHzHZbdw>e-&t_OGoCRweOz&nJm3iD!GfV(M-E1LBHleHCSF zKqmdb2TY(xj0y>JJq=lYc%)IW=_G~l7z3_1Q3wwJ=~|b=R6yQF3Rjayf4Cl`5FWUp zEHHq?0^*y=LvA2k4*hbuqqXYP6CEvAf+aCYI3YvOO`7Q_h`*& zl&h5_Dwt|>7ARJ>p=AG5-L3Ujr?!n!Snn+rbt=N~YS9@d%8V96@fO7*LoO8x#j&(I z(3dL}hx*c~T&i!VFp%ym45qWi%s??al+UX7E`0vM_y4qd`*Y6uyZ$&nbRd|++fv{5 zXXCb1FO6W*b!)|%TSDAD*>GzWXB=^7s&Q=5^)jhUK23v5XYxbY{2>*+?$2$>rP4!( z?oVllz>6-rL;oSw<`0Yx><|$Oxifkq#1%h^F}b%QW5o*f9R%c21hfr$<~V2$vu<9B)GP#=Dcca0GKZgcIA1#M!!z@uNZgK;j!Tq;-G z#>E2vKddq9&{L^2r^=Vs)z0P>t%Oo>F=WgkqQp3Usb8%XCI4*Pk>hO*E1A{y%uE zI^jMTEKg`z-U zisqcG8=a!5<|(@OAw5Npbm$Zv92(3FcIFg4>AinI-*9-o--ieh`~4=+bdPz2cN69W zLU?}vpgcU!5tLnuIZ~}1bt>hb=+}ocK3XX^)Gl|@sWm3(%doCWq4>^frQr}M8F~bj zCd1jqxwBhzM1)vy3Ik4r5c6YAg!tME$2B1y?o>j|4-BL`k`Rx1bf{k!L_Z6mLwv)) zGacSTarm1+hu_1eP+jTIi@It-eO^ur!t-)wp}ABxy(LO>ZZplTT&HR7$DK-Z>1;mR zku*o7qK-hMSbXN%k~kH@q_Z?|q5 zFi+K~ZQnEVol9->dIMEx*W-=Te6vL^n9|0nAjZtsjGXc?-tCXMS542+9{0&(F#{ld z@i>jx5CBCCp>UZ?Uf+DpxS{WL^!L1aLXBveMr$cGJ3^`bh_qX)?RP2%YyO-X&NlfnyLp}hzZm5f0vZLik z-3s=2y_E{)q+{w2dmnm~*Gmj{9aE<#FMi1@5}ejG^6^`oHlo|5U-8VPE6gLuRgKn> zHtBr^^vk?H^XXiU+u%L}{@bh_aDPAC@x~f1%W1!iWnbd7lsAUwKFJ^EwlM~*e9YE2 z_r>iM-u$1#ybi!=-~7v8f6r;$7k#vRcg{J}2lt-k+XOKM>#Z(q12%{fD*!KlDpzSJah z^6Iai&@v}K>QI@Jfh~ji&XIH4tSK#!rdX&f#8U0jGmmOsIrHt(M@!GWOCK%U-Yz{P z2qCe-T>sn>7seibFmEC4zhip9gCK z7*R}}-&22_N5$p#Y+d&%uN83Gl?Sf;gAw4q!2hpNr|pmI)5Q7rB*6WP{y%UPCkO}v zf`A|(2nYg#fFK|U2m(um0QNki`v3Fy6xDyfar*zve=!eb#@>ehKR3Y4o#_8xz3O2Q z(*IvL0eE$9kp90BJ^Tz99Sph~@H%yFpI~x7%$qJOb`)PVIw(d;a+|R}8|ML=oX)DUo8<_y)!2aK7 zGxYy6w$cALb>+VxAP5Kof`A|(2nYg#fFK|U2m*qDAh0|TU>W#!^#7Sh##a2jDzjky zf7pAEy6dC=zm_!hc~zoW9Dusi_67NO&Zqy+LJZ*l<@~?s-I6OoKoAfF1OY)n5D)|e z0YN|zSXu;FhI%=i|HmTNe5P94^8efb4s_Dav>@mIb&+;`$DjYV+Pi6d#Du4P5I+Ae zkV*(*rWW6NoW}vjbia`E|BNj~a|TGb?qbgW;}a;r|C6)=$^ZY_`Y17w7_(kUsD#M> z>zgD0Zg~Xc{6C|kw3+TS)&%K!<)68UX9S*KknM{ednf1rc?TiN`F}V+bO}XZMgCvp z|5K^JKy;gDzWRox+pIU^tXPr%7x{l2jD74y|j^7%!9JB=j zZuQ#E|BG_t^Jxq4Ub`k0Po2K`#6m8^p()#*wm`G|fB4tpG|R=x|MOad8B@s;2N6rBiYvGUlK;A|QvA}|{<9d)nc<76uEO-&fI3PYB&!UgR93I2j5Osa> zxOYY;ER1^5gE)Cvm(vG>mn#kTXiXpBtnHARYI7C{pPkvu2U+&cIP0;wnAUunSu8SS zFc)KKcOY`U?m)W9*6D@MKhU;Loes5ix+RxN4|VMMy2Af2Y%U4^?>!KrKnVXY{J;NR zYN7ucK0-RI!QVm`Hk{o;BQ~7fq9fR1cM1be;YkF6__QG-ft~lJG?~1|vz(D8N z#ZISk=}dkon?FQnS9#w-EO&|T8?EI@A0qdA-y`@Y!D&|bMxyvMzG2|`JLn#Y!}E6! z$}YusPYd$!&I8w<7cEa>_i|bgo|n_2QP7;||1aU}=0Fem2g>=W#iV3vGfQY*Z&n_`l&|6K$McW!m9h7}rJbtTTx-3EBT= zL?pa@5D@;~JV)nSON+9-vi~pp|E^mr*4&ct|NeJK_-n!$AFY%dO8Ea;#Tf^UI8%*d z(GMMBt0Uhg`Tm^n|JP&RqXiub|L-s5%%Hh+HlJAIZ%w@H8E2K$;SLC>hr#$fac94*`=d05`#YjB}(ab+yE?bYYrBc)$W&)Fj8 zIUIOcIKIzp7jGf!Jm94>jH&lUWxpI^;lfeRZB&Ws0 zitce~C^HmY%O1!yt!4k`Q~$2lvI`x$mc3<5I@`I=71kvX{eR3pae{y#AP5Kof`A|( z2nYg#fFN)q5nw&&<)HtMJ#0A0@4%U{w{iX-HvmCr?Mw@z|GzBs|1IyPLHt2Grrm{{ z|Hro;Yc+6OcOm-!EVIvRL|k_v`u}Wd;x5yz;vWJ2|Eubf`A|( z2nYg#fFK|U2m*qDARq_`0)oIzK)@_E#MIMwp7=TBJK#9&{GR&bERn)#d$z87m34+V z?U6hG>wkIuAz<{G|G#X%V;>2UO&9WC5D)|e0YN|z5CjAPK|l}?1Ox#=KoAfF=0E^@ z^D+8+?vwoCfM2hX4ZQi!@Z-AbnM+r~b=Q#<57rl9-k$mXpd1DjQ?1Vb`|Z}^|1tl+ z+8=utoo#s5K1sb9Vf2Qa~9b#57hPRiY^1a{<_bDUZUc#hQK^2 z^P-BZA%ItXUOhp!>w}Z-rsrvw`{(w|Xy&uhA1%F6O~+}t+Ppf(Z3vLL*-qy3ET#|DSF5;XE@qGxj$0|G5EH%Jy0i{r_d5|Ib_JYs}r}`yW#a(f{XLAFPT% z13#t~qW{k}``GFv&zZUO|BaphxIRA*<7FEHY`X%zuTd}BAK5Po|Ias6LP0Z-ZETWQoD+j(HWsn0v5 zfsKA{lJ}{Zmoxph!T)muytym<|JAD=1}oK68T;XF-9WEy7ahjeZdG&dR;j8!J#go^ zQ?HvQX21R2C_s+yqSyPv_L-E#Bg$Ax%-Rr%(%x@O$%ADXafsha0fnZ z8OZ@t`xXCJu^mA0|6A2|>qGlp`=b54{j~i!SpnE*XY93h-2Tk^yY*-5b?bN5v(_)H zW7Z?qcF_jVH(&k<0)l`bAP5Kof`A|(2nYg#z~T{LK4>}c|IBi&_I|DzdmH>eH{e@a zGc7Eq{eQEs98(ZsfUx}tG0rEdwUS{!WBMFK+)J}(9aG>pk5wn!UFDivY*cHHDZ@s` z^eGDdk(quf&04mvQe&9{5r_5PMI4LQ`ljjsFuoOj~y&e&=H2t#H4Nqd^q~nd1bd& z*Zs0=K0;nY;+9~mpN>g(8g9@MV~6!4omVvt@vj2wuG<+M-Lp$0jDCS^y-}MgHl}LM z_;6^wf`2KN07^x};>Ywat9twWF|DH!UgPkE7pU9@^}e;vj*<2M23Y|(MO^<``+ZCJ ze}Yq-ARq_`0)l`bAP5Kof`A|(2rK~th69c%;s1%s z2+YHFBLC0)_#gH@^eD?{aN6m~i(m5AIHz@ueEb%tjnEI*yf)e1V;&h>?dw&qZ|8uQ zZ)V$a=RR>CjWbpWxW6Cncw-Is$7#QeWnbbn%!S3;4&?S)+5h|IU;g@g769;7Pl*3t zuTJ^)|4z{=fY|@z`zE0vAP5Kof`A|(2nYg#fFK|UECm9~iT`I2Yv%LJ*xT6u=LVSJ z6Z!w!R{o#)^^fm<=}D$1PFqU*e{8$L=>MN4{r}Ti|NpWr_WzdR`6g`&0)l`bAP5Ko zg23`aKAE!%-Yy~To@}_aiZhP5Gu1ezGO0{HolfP_ znY`Hl_a62sCHDVGQ9_@lU`{3Ay?7&YK#M`p?`OswLI?M%jHjTWJQWBC1{lX{N4#iI zbjF+Bsz&I*J6FK89_asn%`e=g{!R6_q5t16765$gBq9h10)l`bAP5Kof`A|(2nYg# zz^#UWVKQTMdOhjBYb%SLEdLJ%RrLR%r7*kxKc7E<{r~l9vs!1JTb1fQcSn5X<_Ph= zt(6_(ikBH+Ct(9h(Bj1dCeV5jpa)o5pyX*-Pk=`nu9FnPgL-hii9&cRWze-Qhvf|9 zZKQBDdGv?tK?>nvM*(GFR{`=7h-0L%nGi0Aez}}JoN;&zHX7=xL1rBKbV@jSkW!O6 zH91zTCCinDd$i`jGDH#;Otm=+(2^fY_QME9FCRGBJL43|=3*#bpIIz2yumf-86Gqmz6L#q%fq$_UF$y4AID)M1?5m59{NYPMhFj`9hBv< zAP@5e9{qCOQp}}t#cd4Nj*bx3lGT*SYQ21NN3mF)sx(xiI;p3Wo&up57CE(T7=>A; zGeWukq)}<2iM03sLD-0SFvx4Z_H(xD*mxDoo-Gr9df>qB9X~@>I+N8SNMg?EX})jQ zj*;2sx?gC#L9%|Cjy$xxcqtJeKec zP}d&;b=BgThnH<#wRqZ@j1~>jbIutm4MtzM`BKw!?$uvCp`UX<>d@!hz?Q*$XFliN z!ON65EFDk`@7pAKd=GyM0eO4_!-EgQI~K2KO85CbS`S?68 z7v}Tu0a_{=i+CzbGwQpe2OfSnc@UqU?o_>2OX5wdzZcEFNa0c;Q_80c#qcXMlP>kS zW4STcDLGEcDXRDSesBS#_0%^$9scOu-@Hhs1-Ch+iE?EoFE@Kmgk{Pdii(Tm`t!NW zU^dt4H8^{dv*$E-Mj(>|j>dJ7m_dAH!2ff)go41WkAU$1=9xanJ1pl2845Tp{z&0=Tu^I}kAUNl<@i~j zpUnrbeSbc~0P+%W=8Qq5zHTn*KluL<`|Ko<$)x7)3$xk^l*p^ElwMu-x7n>f@dyWVP2a}@$<3+ l@5%EXJ@)K(YYgK4K0WWZGat|U?Yv*V(0zJt_j<1@{6B+JeP{px diff --git a/backend/tests/fixtures/ldap/ldap_test_data/lock.mdb b/backend/tests/fixtures/ldap/ldap_test_data/lock.mdb deleted file mode 100644 index d09e16ea18316901860137d02e8096a8b905743e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8192 zcmeIuI}SiV5CG7nbTF=@(>Q>;=*2B`uAo#Bzl2DHQu3ylo!y<8;`Qy&L?H@GY#d>@V@@=gr)y{ZdYQ)W?!>%F6uG@_4z;cSZVT{&Bfa0t5&UAV7cs0RjXF5FkK+ X009C72oNAZfB*pk1PBly@DqU diff --git a/backend/tests/integration/README.md b/backend/tests/integration/README.md deleted file mode 100644 index ecbfd8a3..00000000 --- a/backend/tests/integration/README.md +++ /dev/null @@ -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 /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 -``` diff --git a/backend/tests/integration/README_NEW.md b/backend/tests/integration/README_NEW.md deleted file mode 100644 index 17058a7b..00000000 --- a/backend/tests/integration/README_NEW.md +++ /dev/null @@ -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) -- [ ] Добавить тесты для групп и департаментов -- [ ] Добавить тесты для обновления пользователей -- [ ] Протестировать синхронизацию при изменении данных diff --git a/backend/tests/integration/conftest.py b/backend/tests/integration/conftest.py deleted file mode 100644 index 513bb502..00000000 --- a/backend/tests/integration/conftest.py +++ /dev/null @@ -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}") diff --git a/backend/tests/integration/test_ldap_enabled.py b/backend/tests/integration/test_ldap_enabled.py deleted file mode 100644 index d7bf692b..00000000 --- a/backend/tests/integration/test_ldap_enabled.py +++ /dev/null @@ -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}") - - diff --git a/backend/tests/integration/test_ldap_integration.py.old b/backend/tests/integration/test_ldap_integration.py.old deleted file mode 100644 index 0940f381..00000000 --- a/backend/tests/integration/test_ldap_integration.py.old +++ /dev/null @@ -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 diff --git a/backend/tests/integration/test_ldap_simple.py.old b/backend/tests/integration/test_ldap_simple.py.old deleted file mode 100644 index ae098ffa..00000000 --- a/backend/tests/integration/test_ldap_simple.py.old +++ /dev/null @@ -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 diff --git a/backend/tests/ldap_fixtures/01-base-structure.ldif b/backend/tests/ldap_fixtures/01-base-structure.ldif deleted file mode 100644 index f2fa1362..00000000 --- a/backend/tests/ldap_fixtures/01-base-structure.ldif +++ /dev/null @@ -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 diff --git a/backend/tests/manual/conftest.py b/backend/tests/manual/conftest.py deleted file mode 100644 index 057f4f54..00000000 --- a/backend/tests/manual/conftest.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -Конфигурация для manual тестов. -Все тесты в этой директории автоматически помечаются как manual. -""" -import pytest - -# Автоматически помечать все тесты в этой директории как manual -pytestmark = pytest.mark.manual diff --git a/backend/tests/manual/test_announcement_notifications.py b/backend/tests/manual/test_announcement_notifications.py deleted file mode 100644 index 1404bb5f..00000000 --- a/backend/tests/manual/test_announcement_notifications.py +++ /dev/null @@ -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() diff --git a/backend/tests/manual/test_avatar_simple.py b/backend/tests/manual/test_avatar_simple.py deleted file mode 100644 index b4c4faea..00000000 --- a/backend/tests/manual/test_avatar_simple.py +++ /dev/null @@ -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 ") - 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) diff --git a/backend/tests/manual/test_corporate_ip.py b/backend/tests/manual/test_corporate_ip.py deleted file mode 100644 index 14e82f5b..00000000 --- a/backend/tests/manual/test_corporate_ip.py +++ /dev/null @@ -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") diff --git a/backend/tests/manual/test_create_announcement_message.py b/backend/tests/manual/test_create_announcement_message.py deleted file mode 100644 index 979bf3ae..00000000 --- a/backend/tests/manual/test_create_announcement_message.py +++ /dev/null @@ -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() diff --git a/backend/tests/manual/test_email.py b/backend/tests/manual/test_email.py deleted file mode 100644 index 6373d912..00000000 --- a/backend/tests/manual/test_email.py +++ /dev/null @@ -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 " - -# Получатель -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 = """ - - - -

Тестовое письмо

-

Это тестовое письмо для проверки настроек SMTP Яндекса.

-

Если вы получили это письмо, значит настройки корректны.

- - - """ - - 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() diff --git a/backend/tests/manual/test_ip_restrictions.py b/backend/tests/manual/test_ip_restrictions.py deleted file mode 100644 index 8b190a0a..00000000 --- a/backend/tests/manual/test_ip_restrictions.py +++ /dev/null @@ -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() diff --git a/backend/tests/manual/test_ip_simple.py b/backend/tests/manual/test_ip_simple.py deleted file mode 100644 index 4691c53a..00000000 --- a/backend/tests/manual/test_ip_simple.py +++ /dev/null @@ -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()) diff --git a/backend/tests/manual/test_ldap_simple.py b/backend/tests/manual/test_ldap_simple.py deleted file mode 100644 index fdbed769..00000000 --- a/backend/tests/manual/test_ldap_simple.py +++ /dev/null @@ -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 ") - 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) diff --git a/backend/tests/manual/test_notification_create.py b/backend/tests/manual/test_notification_create.py deleted file mode 100644 index 45c817a8..00000000 --- a/backend/tests/manual/test_notification_create.py +++ /dev/null @@ -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() diff --git a/backend/tests/manual/test_notification_debug.py b/backend/tests/manual/test_notification_debug.py deleted file mode 100644 index d11f3ed3..00000000 --- a/backend/tests/manual/test_notification_debug.py +++ /dev/null @@ -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() diff --git a/backend/tests/manual/test_notification_signals.py b/backend/tests/manual/test_notification_signals.py deleted file mode 100644 index b510bc4e..00000000 --- a/backend/tests/manual/test_notification_signals.py +++ /dev/null @@ -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() diff --git a/backend/tests/manual/test_pagination.py b/backend/tests/manual/test_pagination.py deleted file mode 100644 index 48e75168..00000000 --- a/backend/tests/manual/test_pagination.py +++ /dev/null @@ -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() diff --git a/backend/tests/manual/test_recipients.py b/backend/tests/manual/test_recipients.py deleted file mode 100644 index cfdcaa4e..00000000 --- a/backend/tests/manual/test_recipients.py +++ /dev/null @@ -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) diff --git a/backend/tests/manual/test_recipients_api.py b/backend/tests/manual/test_recipients_api.py deleted file mode 100644 index c1ae6f11..00000000 --- a/backend/tests/manual/test_recipients_api.py +++ /dev/null @@ -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}") diff --git a/backend/tests/manual/test_register_api.py b/backend/tests/manual/test_register_api.py deleted file mode 100644 index 6e2aa17c..00000000 --- a/backend/tests/manual/test_register_api.py +++ /dev/null @@ -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) diff --git a/backend/tests/manual/test_reply_update.py b/backend/tests/manual/test_reply_update.py deleted file mode 100644 index 180ca56f..00000000 --- a/backend/tests/manual/test_reply_update.py +++ /dev/null @@ -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() diff --git a/backend/tests/manual/test_requests_access.py b/backend/tests/manual/test_requests_access.py deleted file mode 100644 index 9b807689..00000000 --- a/backend/tests/manual/test_requests_access.py +++ /dev/null @@ -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() diff --git a/backend/tests/manual/test_requests_access_detailed.py b/backend/tests/manual/test_requests_access_detailed.py deleted file mode 100644 index 9a223cb1..00000000 --- a/backend/tests/manual/test_requests_access_detailed.py +++ /dev/null @@ -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() diff --git a/backend/tests/middleware/test_email_verification_middleware.py b/backend/tests/middleware/test_email_verification_middleware.py deleted file mode 100644 index 8b38cfab..00000000 --- a/backend/tests/middleware/test_email_verification_middleware.py +++ /dev/null @@ -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] diff --git a/backend/tests/notifications/__init__.py b/backend/tests/notifications/__init__.py deleted file mode 100644 index 62a4befe..00000000 --- a/backend/tests/notifications/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# Notifications tests package diff --git a/backend/tests/notifications/test_api.py b/backend/tests/notifications/test_api.py deleted file mode 100644 index bfe2b4ea..00000000 --- a/backend/tests/notifications/test_api.py +++ /dev/null @@ -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) diff --git a/backend/tests/notifications/test_models.py b/backend/tests/notifications/test_models.py deleted file mode 100644 index 23dc7b7a..00000000 --- a/backend/tests/notifications/test_models.py +++ /dev/null @@ -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' - ) diff --git a/backend/tests/notifications/test_services.py b/backend/tests/notifications/test_services.py deleted file mode 100644 index e26ae710..00000000 --- a/backend/tests/notifications/test_services.py +++ /dev/null @@ -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) diff --git a/backend/tests/requests_app/conftest.py b/backend/tests/requests_app/conftest.py deleted file mode 100644 index a8b975af..00000000 --- a/backend/tests/requests_app/conftest.py +++ /dev/null @@ -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 - diff --git a/backend/tests/requests_app/test_filters_basic.py b/backend/tests/requests_app/test_filters_basic.py deleted file mode 100644 index aa226d30..00000000 --- a/backend/tests/requests_app/test_filters_basic.py +++ /dev/null @@ -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} diff --git a/backend/tests/requests_app/test_forms.py b/backend/tests/requests_app/test_forms.py deleted file mode 100644 index 5a44edaa..00000000 --- a/backend/tests/requests_app/test_forms.py +++ /dev/null @@ -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 diff --git a/backend/tests/requests_app/test_list_requests.py b/backend/tests/requests_app/test_list_requests.py deleted file mode 100644 index b78741dc..00000000 --- a/backend/tests/requests_app/test_list_requests.py +++ /dev/null @@ -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 diff --git a/backend/tests/requests_app/test_my_and_all.py b/backend/tests/requests_app/test_my_and_all.py deleted file mode 100644 index 0753c6d0..00000000 --- a/backend/tests/requests_app/test_my_and_all.py +++ /dev/null @@ -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 diff --git a/backend/tests/requests_app/test_process.py b/backend/tests/requests_app/test_process.py deleted file mode 100644 index 6283cc0a..00000000 --- a/backend/tests/requests_app/test_process.py +++ /dev/null @@ -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 # статус не изменился diff --git a/backend/tests/requests_app/test_rate_limit_fallback.py b/backend/tests/requests_app/test_rate_limit_fallback.py deleted file mode 100644 index 9e0b68bd..00000000 --- a/backend/tests/requests_app/test_rate_limit_fallback.py +++ /dev/null @@ -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 diff --git a/backend/tests/requests_app/test_safe_back_url.py b/backend/tests/requests_app/test_safe_back_url.py deleted file mode 100644 index 6ecf4d63..00000000 --- a/backend/tests/requests_app/test_safe_back_url.py +++ /dev/null @@ -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"] diff --git a/backend/tests/requests_app_fixtures.py b/backend/tests/requests_app_fixtures.py deleted file mode 100644 index a066e436..00000000 --- a/backend/tests/requests_app_fixtures.py +++ /dev/null @@ -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 diff --git a/backend/tests/test_bidirectional_chat_loading.py b/backend/tests/test_bidirectional_chat_loading.py deleted file mode 100644 index c0055292..00000000 --- a/backend/tests/test_bidirectional_chat_loading.py +++ /dev/null @@ -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']) diff --git a/backend/tests/test_static_files.py b/backend/tests/test_static_files.py deleted file mode 100644 index e33f8bc3..00000000 --- a/backend/tests/test_static_files.py +++ /dev/null @@ -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" - ) diff --git a/backend/tests/test_template_components.py b/backend/tests/test_template_components.py deleted file mode 100644 index 8f2393fc..00000000 --- a/backend/tests/test_template_components.py +++ /dev/null @@ -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}") diff --git a/backend/tests/textarea_autoexpand_test.html b/backend/tests/textarea_autoexpand_test.html deleted file mode 100644 index bf28beee..00000000 --- a/backend/tests/textarea_autoexpand_test.html +++ /dev/null @@ -1,196 +0,0 @@ - - - - - - Тест автоматического расширения textarea - - - - -

🔧 Тест автоматического расширения textarea

- -
- Инструкция: Начните вводить текст в каждое поле. Поля должны автоматически расширяться до максимум 6 строк (144px). -
- - -
-

1. Быстрый комментарий в ленте (rows="1")

-
- -
-
⏳ Ожидание ввода...
-
- - -
-

2. Комментарий в модальном окне (rows="1")

-
- -
-
⏳ Ожидание ввода...
-
- - -
-

3. Комментарий к заявке (rows="1")

-
- -
-
⏳ Ожидание ввода...
-
- - -
-

4. Поле ввода в чате (composer-textarea)

- -
⏳ Ожидание ввода...
-
- - -
-

5. Textarea БЕЗ автоматического расширения (rows="3")

- -
⏳ Должно остаться на 3 строках
-
- - -
-

6. Динамически добавленный textarea

- -
-
⏳ Нажмите кнопку
-
- - -
-
-
📊 Результаты тестирования
-
    -
  • Инициализация модуля...
  • -
-
-
- - - - - - - -