From 6cebd772f2b591f5dbf3000206fedd16ac303897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Mon, 5 Jan 2026 10:59:07 +0300 Subject: [PATCH] - --- .../emails/password_reset_email.html | 4 +- docs/reports/AUTH_TESTS_ANALYSIS.md | 319 +++++++++++++++ docs/reports/PASSWORD_RECOVERY_ANALYSIS.md | 374 ++++++++++++++++++ .../REGISTRATION_ERROR_SYNOLOGY_ANALYSIS.md | 206 ++++++++++ docs/reports/TESTS_REFACTORING_REPORT.md | 6 +- 5 files changed, 904 insertions(+), 5 deletions(-) create mode 100644 docs/reports/AUTH_TESTS_ANALYSIS.md create mode 100644 docs/reports/PASSWORD_RECOVERY_ANALYSIS.md create mode 100644 docs/reports/REGISTRATION_ERROR_SYNOLOGY_ANALYSIS.md diff --git a/backend/templates/emails/password_reset_email.html b/backend/templates/emails/password_reset_email.html index 8c860d29..42f61310 100644 --- a/backend/templates/emails/password_reset_email.html +++ b/backend/templates/emails/password_reset_email.html @@ -2,10 +2,10 @@

Сброс пароля

-

Здравствуйте{{ user.get_full_name|default_if_none:"" }}!

+

Здравствуйте {{ user.get_full_name|default_if_none:"" }}!

Вы запросили сброс пароля. Нажмите на кнопку ниже для смены пароля:

- Сбросить пароль diff --git a/docs/reports/AUTH_TESTS_ANALYSIS.md b/docs/reports/AUTH_TESTS_ANALYSIS.md new file mode 100644 index 00000000..a23dbb44 --- /dev/null +++ b/docs/reports/AUTH_TESTS_ANALYSIS.md @@ -0,0 +1,319 @@ +# 🔍 Подробный отчет: Проблемы с тестами аутентификации + +**Дата:** 5 января 2026 +**Файл:** `tests/api/auth/test_auth_and_registration.py` + +## 📊 Статистика + +``` +✅ 8 тестов ПРОШЛИ +❌ 11 тестов ПРОВАЛЕНЫ +━━━━━━━━━━━━━━━━━━━━━ +📈 Успешность: 42% +``` + +## 🔴 ОСНОВНАЯ ПРОБЛЕМА + +### 🚨 КРИТИЧЕСКОЕ НЕСООТВЕТСТВИЕ: Модель vs Сериализатор + +#### ✅ Модель Employee (models.py:162-165): +```python +gender = models.PositiveSmallIntegerField( + "Пол", choices=GENDER_CHOICES, + default=0, # ✅ Есть дефолт + blank=True # ✅ НЕОБЯЗАТЕЛЬНОЕ +) +avatar = models.ImageField( + "Фото", upload_to="users/avatars", + blank=True, # ✅ НЕОБЯЗАТЕЛЬНОЕ + null=True # ✅ НЕОБЯЗАТЕЛЬНОЕ +) +``` + +#### ❌ RegisterSerializer (serializers.py:29-67): +```python +class RegisterSerializer(serializers.Serializer): + # ... другие поля ... + + avatar = Base64ImageField(required=True) # ❌ ОБЯЗАТЕЛЬНОЕ (противоречие!) + + gender = serializers.ChoiceField( + required=True, # ❌ ОБЯЗАТЕЛЬНОЕ (противоречие!) + choices=((1, "Мужской"), (2, "Женский")), + error_messages={ + 'required': 'Поле "Пол" обязательно для заполнения.', + } + ) +``` + +**⚠️ Проблема:** Сериализатор требует поля, которые в модели **необязательны**. Это создает искусственные требования для API регистрации, не соответствующие структуре БД. + +--- + +### Обязательные поля в RegisterSerializer + +**Файл:** `backend/api/v1/employees/serializers.py` (строки 29-67) + +```python +class RegisterSerializer(serializers.Serializer): + # ... другие поля ... + + avatar = Base64ImageField(required=True) # ❌ ОБЯЗАТЕЛЬНОЕ + + gender = serializers.ChoiceField( + required=True, # ❌ ОБЯЗАТЕЛЬНОЕ + choices=((1, "Мужской"), (2, "Женский")), + error_messages={ + 'required': 'Поле "Пол" обязательно для заполнения.', + } + ) +``` + +### Что отправляют тесты + +**Файл:** `tests/api/auth/test_auth_and_registration.py` (строка 44) + +```python +def register_payload(**overrides): + data = { + "first_name": "Иван", + "last_name": "Иванов", + "phone_number": "+79990000001", + "email": "ivan@example.com", + "password": "Str0ngPass!", + "birth_date": "1990-01-01", + "telegram": "@ivan", + # ❌ НЕТ avatar + # ❌ НЕТ gender + } + return data +``` + +## 📝 Ошибки валидации + +Все 11 проваленных тестов получают одинаковую ошибку: + +```python +[REGISTER] Validation errors: { + 'avatar': [ErrorDetail(string='Ни одного файла не было отправлено.', code='required')], + 'gender': [ErrorDetail(string='Поле "Пол" обязательно для заполнения.', code='required')] +} +``` + +**HTTP статус:** `400 Bad Request` (ожидается `201 Created`) + +## 📋 Детальный список проваленных тестов + +### 1. ❌ test_register_success_sends_email_and_user_inactive +**Цель:** Проверка успешной регистрации с отправкой email +**Проблема:** Не передает `avatar` и `gender` + +### 2. ❌ test_register_duplicate_email_phone +**Цель:** Проверка дубликатов email/phone +**Проблема:** Не может создать первого пользователя (нет `avatar`, `gender`) + +### 3. ❌ test_register_accepts_optional_fields +**Цель:** Проверка опциональных полей (patronymic, whatsapp) +**Проблема:** Передает `gender=1`, но **не передает `avatar`** + +```python +# Частичное исправление в тесте: +payload = { + ... + "patronymic": "Петрович", + "gender": 1, # ✅ Есть + "whatsapp": "+79995550011", + # ❌ Нет avatar +} +``` + +### 4-11. ❌ Тесты верификации email и логина +Все зависят от `register()` функции, которая не передает обязательные поля: +- `test_verify_email_success_activates_user` +- `test_verify_email_wrong_code` +- `test_resend_email_sends_new_code_and_old_becomes_invalid` +- `test_verify_expired_more_than_5_minutes_deletes_account` +- `test_login_by_email_denied_before_verify` +- `test_login_by_email_allowed_after_verify` +- `test_login_by_phone_allowed_after_verify` +- `test_login_wrong_password` + +## ✅ Успешные тесты (8 штук) + +Эти тесты проверяют **валидацию отсутствующих полей**, поэтому они ожидают `400 Bad Request`: + +1. ✅ `test_register_missing_required_field[first_name]` +2. ✅ `test_register_missing_required_field[last_name]` +3. ✅ `test_register_missing_required_field[phone_number]` +4. ✅ `test_register_missing_required_field[email]` +5. ✅ `test_register_missing_required_field[password]` +6. ✅ `test_register_missing_required_field[birth_date]` +7. ✅ `test_register_requires_at_least_one_contact` +8. ✅ `test_register_birth_date_must_be_valid_date` + +**Почему они проходят?** Они ожидают ошибку валидации и получают её (хоть и по другим причинам). + +## 🔍 Анализ причин + +### Вопрос 1: Почему `avatar` и `gender` обязательные? + +Вероятно, это бизнес-требование: +- **Avatar:** Для корпоративной системы важно иметь фото сотрудника +- **Gender:** Для HR-целей и документооборота + +### Вопрос 2: Должны ли они быть обязательными при регистрации? + +**Два подхода:** + +#### Вариант A: Сделать опциональными для регистрации +```python +avatar = Base64ImageField(required=False, allow_null=True) +gender = serializers.ChoiceField( + required=False, + allow_null=True, + choices=((1, "Мужской"), (2, "Женской")), +) +``` + +**Плюсы:** Упрощает регистрацию +**Минусы:** Может нарушить бизнес-логику + +#### Вариант B: Исправить тесты (добавить поля) +```python +def register_payload(**overrides): + data = { + "first_name": "Иван", + "last_name": "Иванов", + "phone_number": "+79990000001", + "email": "ivan@example.com", + "password": "Str0ngPass!", + "birth_date": "1990-01-01", + "telegram": "@ivan", + "avatar": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", # 1x1 прозрачный PNG + "gender": 1, # Мужской + } + return data +``` + +**Плюсы:** Соответствует текущей бизнес-логике +**Минусы:** Нужно обновить все тесты + +## 🎯 Рекомендации + +### ✅ РЕКОМЕНДУЕМОЕ РЕШЕНИЕ: Исправить сериализатор + +**Причина:** Модель Employee позволяет `avatar` и `gender` быть необязательными. Сериализатор должен соответствовать модели. + +**Изменение в `backend/api/v1/employees/serializers.py`:** + +```python +class RegisterSerializer(serializers.Serializer): + # ... другие поля ... + + # Было: + # avatar = Base64ImageField(required=True) + # gender = serializers.ChoiceField(required=True, ...) + + # Стало: + avatar = Base64ImageField(required=False, allow_null=True) # ✅ Соответствует модели + + gender = serializers.ChoiceField( + required=False, # ✅ Соответствует модели + choices=((1, "Мужской"), (2, "Женский"), (0, "Не указано")), + default=0, # ✅ Соответствует модели + error_messages={ + 'invalid_choice': 'Выберите корректное значение пола.', + } + ) +``` + +**Преимущества:** +- ✅ Соответствие архитектуре (модель → сериализатор → API) +- ✅ Упрощение регистрации для пользователей +- ✅ Тесты пройдут без изменений +- ✅ Возможность заполнить профиль позже + +--- + +### Альтернатива: Обновить тесты (НЕ рекомендуется) + +**Обновить `register_payload()` в тестах:** + +```python +def register_payload(**overrides): + # Минимальное base64 изображение (1x1 пиксель) + MINIMAL_AVATAR = ( + "data:image/png;base64," + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" + ) + + data = { + "first_name": "Иван", + "last_name": "Иванов", + "phone_number": "+79990000001", + "email": "ivan@example.com", + "password": "Str0ngPass!", + "birth_date": "1990-01-01", + "telegram": "@ivan", + "avatar": MINIMAL_AVATAR, # ✅ Добавлено + "gender": 1, # ✅ Добавлено (1 = Мужской) + } + data.update(overrides) + return data +``` + +**Недостатки этого подхода:** +- ❌ Не решает архитектурную проблему (несоответствие модели и сериализатора) +- ❌ Усложняет регистрацию в production (требует обязательную загрузку аватара) +- ❌ Требует изменений в тестах вместо исправления кода + +--- + +### Долгосрочное решение (архитектурное) + +**Вариант 1:** Разделить регистрацию на этапы +1. **Этап 1:** Email, пароль, базовая информация +2. **Этап 2:** После верификации email → заполнение профиля (avatar, gender) + +**Вариант 2:** Сделать поля условно обязательными +- При регистрации: опциональные +- При активации аккаунта: требуется заполнить +- При первом входе: напоминание заполнить + +## 📊 Влияние на production + +### ⚠️ Текущая ситуация + +Если `avatar` и `gender` обязательны: +- Фронтенд **должен** отправлять эти поля при регистрации +- Если фронтенд не отправляет → регистрация не работает +- Пользователи не могут зарегистрироваться + +### ✅ Вопросы для проверки + +1. **Работает ли регистрация на production?** + - Если да → значит фронтенд корректно отправляет поля + - Если нет → критическая ошибка + +2. **Как фронтенд отправляет avatar?** + - Base64 строка + - File upload + - URL изображения + +3. **Обязателен ли gender в бизнес-логике?** + - Требование HR + - Требование законодательства + - Опционально + +## 🚀 Заключение + +**Основная проблема:** Рассинхронизация между тестами и сериализатором + +- **Сериализатор** требует `avatar` и `gender` +- **Тесты** не передают эти поля + +**Решение:** Нужно привести в соответствие: +- ЛИБО сделать поля опциональными в сериализаторе +- ЛИБО добавить поля в тестовые данные + +**Рекомендация:** Обновить тесты (добавить `avatar` и `gender`), так как это, вероятно, соответствует реальному поведению фронтенда. diff --git a/docs/reports/PASSWORD_RECOVERY_ANALYSIS.md b/docs/reports/PASSWORD_RECOVERY_ANALYSIS.md new file mode 100644 index 00000000..18bf9bfb --- /dev/null +++ b/docs/reports/PASSWORD_RECOVERY_ANALYSIS.md @@ -0,0 +1,374 @@ +# 🔍 Анализ: Восстановление пароля не отправляет email + +**Дата:** 5 января 2026 +**Статус:** ⚠️ Email не отправляется при восстановлении пароля +**Тип:** Логика фильтрации пользователей в Django PasswordResetForm + +## 📋 Проблема + +Пользователи жалуются, что при нажатии кнопки "Забыли пароль?" не приходит письмо на почту. + +### Логи системы: + +``` +[INFO] 08:52:00 HTTP GET /auth/password-reset/ 200 ← Открыта форма +[INFO] 08:52:24 HTTP POST /auth/password-reset/ 302 ← Форма отправлена +[INFO] 08:52:24 HTTP GET /auth/password-reset/done/ 200 ← Показана страница "готово" +``` + +**Проблема:** Нет записей об отправке email! Ожидалось что-то вроде: +``` +[INFO] Email отправлен: password_reset → user@example.com +``` + +## 🔍 Исследование + +### Код восстановления пароля + +**Используется:** Django встроенный `PasswordResetView` и `PasswordResetForm` + +[employees/views_auth.py](backend/employees/views_auth.py#L217-L222): +```python +class PasswordResetView(DjangoPasswordResetView): + template_name = "auth/password_reset.html" + email_template_name = "auth/password_reset_email.txt" + subject_template_name = "auth/password_reset_subject.txt" + success_url = "/auth/password-reset/done/" +``` + +### Django PasswordResetForm.get_users() + +Проверил исходный код Django `PasswordResetForm`: + +```python +def get_users(self, email): + """Given an email, return matching user(s) who should receive a reset. + + This allows subclasses to more easily customize the default policies + that prevent inactive users and users with unusable passwords from + resetting their password. + """ + email_field_name = UserModel.get_email_field_name() + active_users = UserModel._default_manager.filter( + **{ + "%s__iexact" % email_field_name: email, + "is_active": True, # ← 🚨 ФИЛЬТР НА АКТИВНЫХ! + } + ) + return ( + u + for u in active_users + if u.has_usable_password() + and _unicode_ci_compare(email, getattr(u, email_field_name)) + ) +``` + +**Ключевая строка:** `"is_active": True` + +### Логика регистрации в EUSRR + +[api/v1/employees/views.py](backend/api/v1/employees/views.py#L473-L477): +```python +# Режим без LDAP: создаём пользователя напрямую в БД +emp = Employee.objects.create( + first_name=v["first_name"], + last_name=v["last_name"], + email=email, + phone_number=phone_norm, + is_active=False, # ← не активен до верификации email + is_ldap_managed=False, +) +``` + +Пользователь становится активным только после верификации email: + +[api/v1/employees/views.py](backend/api/v1/employees/views.py#L1778-L1788): +```python +# Верификация email +emp = Employee.objects.filter(email__iexact=email).first() +# ... проверка кода ... + +# Активируем пользователя +emp.is_active = True # ← Активируем +emp.email_verified = True +emp.save(update_fields=["is_active", "email_verified", "email_activation_code"]) +``` + +## 🎯 КОРНЕВАЯ ПРИЧИНА + +### Сценарий 1: Незаверифицированный пользователь + +1. **Пользователь регистрируется** → `is_active=False`, `email_verified=False` +2. **НЕ верифицирует email** (не ввел код из письма) +3. **Пытается восстановить пароль** +4. Django `PasswordResetForm.get_users()` фильтрует: `is_active=True` +5. **Пользователь не найден** → email НЕ отправляется +6. Но форма показывает "done" (чтобы не раскрывать существование аккаунта) + +### Сценарий 2: Верифицированный пользователь + +1. **Пользователь регистрируется** → `is_active=False`, `email_verified=False` +2. **Верифицирует email** → `is_active=True`, `email_verified=True` +3. **Пытается восстановить пароль** +4. Django `PasswordResetForm.get_users()` находит пользователя +5. **Email отправляется** ✅ + +## 📊 Диагностика + +### Проверка текущего статуса пользователей: + +```bash +.venv/Scripts/python manage.py shell +``` + +```python +from employees.models import Employee + +# Незаверифицированные пользователи +unverified = Employee.objects.filter( + is_active=False, + email_verified=False +).count() +print(f"Незаверифицированных: {unverified}") + +# Заверифицированные пользователи +verified = Employee.objects.filter( + is_active=True, + email_verified=True +).count() +print(f"Заверифицированных: {verified}") + +# Проблемные: email verified, но is_active=False (не должны существовать) +problem = Employee.objects.filter( + is_active=False, + email_verified=True +).count() +print(f"Проблемных: {problem}") +``` + +### Тест восстановления пароля: + +```bash +.venv/Scripts/python manage.py shell +``` + +```python +from django.contrib.auth.forms import PasswordResetForm +from employees.models import Employee + +# Создать тестового неактивного пользователя +test_user = Employee.objects.create( + email="test@example.com", + first_name="Test", + last_name="User", + phone_number="+79990001234", + is_active=False, # НЕ активен + telegram="@test" +) +test_user.set_password("password123") +test_user.save() + +# Попытка сброса пароля +form = PasswordResetForm(data={"email": "test@example.com"}) +if form.is_valid(): + users = list(form.get_users(form.cleaned_data["email"])) + print(f"Найдено пользователей: {len(users)}") # ← Должно быть 0! +else: + print("Форма невалидна") + +# Очистка +test_user.delete() +``` + +**Ожидаемый результат:** `Найдено пользователей: 0` для неактивного пользователя. + +## ✅ РЕШЕНИЯ + +### Вариант 1: Переопределить get_users() (рекомендуется) + +Создать кастомную форму, которая позволяет сбрасывать пароль незаверифицированным пользователям: + +**Создать:** `backend/employees/forms_auth.py` (дополнение) + +```python +from django.contrib.auth.forms import PasswordResetForm as DjangoPasswordResetForm +from django.contrib.auth import get_user_model + +class CustomPasswordResetForm(DjangoPasswordResetForm): + """Форма сброса пароля, разрешающая сброс для неактивных пользователей""" + + def get_users(self, email): + """Находит пользователей по email, включая неактивных. + + Отличие от Django: не фильтрует по is_active=True, + чтобы пользователи могли восстановить пароль до верификации email. + """ + UserModel = get_user_model() + email_field_name = UserModel.get_email_field_name() + + # Убираем фильтр is_active=True + users = UserModel._default_manager.filter( + **{f"{email_field_name}__iexact": email} + ) + + return ( + u for u in users + if u.has_usable_password() + and u.email.lower() == email.lower() + ) +``` + +**Обновить:** `backend/employees/views_auth.py` + +```python +from .forms_auth import CustomPasswordResetForm + +class PasswordResetView(DjangoPasswordResetView): + template_name = "auth/password_reset.html" + email_template_name = "auth/password_reset_email.txt" + subject_template_name = "auth/password_reset_subject.txt" + success_url = "/auth/password-reset/done/" + form_class = CustomPasswordResetForm # ← Использовать кастомную форму +``` + +**Преимущества:** +- ✅ Пользователи могут восстановить пароль до верификации email +- ✅ Стандартное поведение Django, просто без фильтра `is_active` +- ✅ Безопасно (форма все равно не раскрывает существование аккаунта) + +**Недостатки:** +- ⚠️ Пользователи без верификации email могут получить доступ + +### Вариант 2: Активировать при регистрации + +Изменить логику: `is_active=True` сразу, но блокировать вход через middleware до верификации. + +**Изменить:** `backend/api/v1/employees/views.py` + +```python +emp = Employee.objects.create( + # ... + is_active=True, # ← Активен сразу + email_verified=False, # ← Но email не подтвержден + is_ldap_managed=False, +) +``` + +**Создать middleware:** `backend/eusrr_backend/middleware.py` (дополнение) + +```python +class EmailVerificationRequiredMiddleware: + """Блокирует доступ пользователям без верификации email""" + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if request.user.is_authenticated: + if not request.user.email_verified: + # Разрешить только страницы верификации + allowed = [ + '/auth/verify-email/', + '/auth/resend-email/', + '/auth/logout/', + ] + if not any(request.path.startswith(p) for p in allowed): + return redirect('auth_front:verify_email') + + return self.get_response(request) +``` + +**Преимущества:** +- ✅ Восстановление пароля работает для всех +- ✅ Безопасность через middleware + +**Недостатки:** +- ⚠️ Более сложная логика +- ⚠️ Требует тщательного тестирования + +### Вариант 3: Разрешить вход без верификации (НЕ рекомендуется) + +Просто убрать требование верификации email вообще. + +**Недостатки:** +- ❌ Небезопасно (фейковые email) +- ❌ Не соответствует логике системы + +## 🧪 Тестирование после фикса + +### 1. Тест незаверифицированного пользователя: + +```python +# 1. Создать пользователя (is_active=False) +# 2. Попытаться восстановить пароль +# 3. Проверить что email отправлен +# 4. Перейти по ссылке из письма +# 5. Установить новый пароль +# 6. Войти с новым паролем +``` + +### 2. Тест заверифицированного пользователя: + +```python +# 1. Создать пользователя и верифицировать email +# 2. Попытаться восстановить пароль +# 3. Проверить что email отправлен +# 4. Установить новый пароль +# 5. Войти +``` + +## 📈 Дополнительные проверки + +### Настройки EMAIL + +Проверить [settings.py](backend/eusrr_backend/settings.py#L233-L246): + +```python +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = 'smtp.yandex.ru' +EMAIL_PORT = 465 +EMAIL_HOST_USER = 'robotail-info@yandex.ru' +EMAIL_HOST_PASSWORD = '***' # Проверить что пароль валиден +EMAIL_USE_SSL = True +``` + +**Тест отправки email:** + +```bash +.venv/Scripts/python manage.py shell +``` + +```python +from django.core.mail import send_mail + +send_mail( + 'Test Subject', + 'Test Message', + 'robotail-info@yandex.ru', + ['recipient@example.com'], + fail_silently=False, +) +# Если ошибка → проверить настройки SMTP +``` + +### Шаблоны писем + +Проверить существование: +- ✅ `backend/templates/auth/password_reset_email.txt` +- ✅ `backend/templates/auth/password_reset_subject.txt` + +## 🎯 Рекомендация + +**Использовать Вариант 1** - переопределить `get_users()`: + +1. Минимальные изменения кода +2. Решает проблему напрямую +3. Безопасно (не раскрывает существование аккаунтов) +4. Пользователи смогут восстановить пароль в любом статусе + +**Дополнительно:** Добавить в письмо восстановления пароля ссылку на верификацию email, если он не подтвержден. + +--- + +**Приоритет:** 🔴 ВЫСОКИЙ - пользователи не могут восстановить доступ +**Трудозатраты:** ⚡ 10-15 минут на реализацию Варианта 1 diff --git a/docs/reports/REGISTRATION_ERROR_SYNOLOGY_ANALYSIS.md b/docs/reports/REGISTRATION_ERROR_SYNOLOGY_ANALYSIS.md new file mode 100644 index 00000000..5c558649 --- /dev/null +++ b/docs/reports/REGISTRATION_ERROR_SYNOLOGY_ANALYSIS.md @@ -0,0 +1,206 @@ +# 🔴 КРИТИЧЕСКАЯ ОШИБКА: Регистрация падает с IntegrityError + +**Дата:** 5 января 2026 +**Статус:** 🚨 БЛОКИРУЕТ регистрацию пользователей +**Тип:** Синхронизация БД и модели - несоответствие + +## 📋 Проблема + +``` +IntegrityError at /api/v1/auth/register/ +NOT NULL constraint failed: employees_employee.synology_username +``` + +### Что происходит? + +1. **Пользователь отправляет данные на** `/api/v1/auth/register/` +2. **Django пытается создать Employee** с помощью `Employee.objects.create()` +3. **SQLite попадает на колонки в БД**, которых НЕТ в модели Django +4. **Результат:** пустые значения для этих колонок +5. **БД отвергает:** поле `synology_username` помечено как `NOT NULL` → ошибка + +## 🔍 Диагностика + +### В БД (SQLite) существуют колонки: + +```sql +PRAGMA table_info(employees_employee); + +-- Результат grep: +(25, 'synology_user_id', 'INTEGER', 0, None, 0) +(26, 'synology_username', 'varchar(100)', 1, None, 0) -- ❌ NOT NULL! +(27, 'synology_session_expires', 'datetime', 0, None, 0) +(28, 'synology_session_id', 'varchar(255)', 1, None, 0) -- ❌ NOT NULL! +``` + +**Пояснение колонок (слева направо):** +- `0` = порядковый номер +- `'synology_username'` = название колонки +- `'varchar(100)'` = тип данных +- `1` = NOT NULL constraint (1 = есть constraint, 0 = нет) +- `None` = default значение +- `0` = PRIMARY KEY flag + +### В модели (employees/models.py): + +```python +class Employee(AbstractUser): + # ... другие поля ... + + # ❌ SYNOLOGY ПОЛЕЙ НЕТ! + # synology_user_id - не определено + # synology_username - не определено + # synology_session_expires - не определено + # synology_session_id - не определено +``` + +## 🚨 КОРНЕВАЯ ПРИЧИНА + +### Сценарий что произошло: + +1. **На ранней стадии разработки** были добавлены Synology поля в БД (миграция была создана) +2. **Позже эти поля были удалены из модели** Employee (вероятно, закомментированы или удалены) +3. **НО соответствующая миграция удаления никогда не была создана** (`RemoveField`) +4. **Результат:** Мисматч между: + - ✅ Тем, что Django ожидает создать (модель) + - ⚠️ Тем, что реально в БД (таблица с Synology полями) + +### Почему ошибка происходит именно при регистрации? + +Когда Django создаёт объект: +```python +emp = Employee.objects.create( + first_name=v["first_name"], + last_name=v["last_name"], + email=email, + phone_number=phone_norm, + is_active=False, + is_ldap_managed=False, + # ❌ synology_username НЕ УКАЗАН (его нет в модели) + # ❌ synology_session_id НЕ УКАЗАН (его нет в модели) +) +``` + +SQLite вставляет `NULL` для недостающих полей, но: +- `synology_username` имеет `NOT NULL` constraint → **ОШИБКА** +- `synology_session_id` имеет `NOT NULL` constraint → **ОШИБКА** + +## ✅ РЕШЕНИЕ + +### Вариант 1: Удалить поля из БД (рекомендуется) + +Создать миграцию удаления: + +```bash +.venv/Scripts/python manage.py makemigrations employees --empty --name remove_synology_fields +``` + +Затем заполнить миграцию: + +```python +from django.db import migrations + +class Migration(migrations.Migration): + dependencies = [ + ('employees', 'PREV_MIGRATION'), # последняя миграция + ] + + operations = [ + migrations.RemoveField( + model_name='employee', + name='synology_user_id', + ), + migrations.RemoveField( + model_name='employee', + name='synology_username', + ), + migrations.RemoveField( + model_name='employee', + name='synology_session_expires', + ), + migrations.RemoveField( + model_name='employee', + name='synology_session_id', + ), + ] +``` + +Затем применить: + +```bash +.venv/Scripts/python manage.py migrate +``` + +### Вариант 2: Вернуть поля в модель + +Если Synology интеграция ещё нужна, вернуть поля в модель: + +```python +class Employee(AbstractUser): + # ... другие поля ... + + synology_user_id = models.IntegerField(null=True, blank=True) + synology_username = models.CharField( + max_length=100, + blank=True, + null=True, # ← Чтобы Django мог вставлять NULL + verbose_name="Synology Username" + ) + synology_session_expires = models.DateTimeField(null=True, blank=True) + synology_session_id = models.CharField( + max_length=255, + blank=True, + null=True, # ← Чтобы Django мог вставлять NULL + verbose_name="Synology Session ID" + ) +``` + +Затем создать миграцию: + +```bash +.venv/Scripts/python manage.py makemigrations +``` + +**Внимание:** Миграция обнаружит уже существующие поля в БД, может показать `AlterField` вместо `AddField`. + +### Вариант 3: Очистить и пересоздать БД (только для разработки) + +```bash +rm backend/db.sqlite3 +.venv/Scripts/python manage.py migrate +``` + +## 📊 Рекомендация + +**Используйте Вариант 1** - удаление полей: + +1. Synology поля очевидно не используются (они отсутствуют в коде) +2. Это чистит БД и приводит её в соответствие с моделью +3. Регистрация сразу же начнёт работать +4. Если Synology интеграция понадобится позже, её легко добавить заново + +## 🔗 Связанные проблемы + +Эта ошибка влияет на: +- ❌ **Регистрация пользователей** (новые пользователи не могут зарегистрироваться) +- ❌ **API endpoint** `/api/v1/auth/register/` +- ❌ **Фронт-end регистрация** (через views_auth.py) +- ✅ Всё остальное (логин, профиль) работает нормально + +## 🧪 Тестирование после фикса + +После удаления полей: + +1. Попробовать регистрацию через API +2. Проверить, что пользователь создан в БД +3. Выполнить `python manage.py check` для валидации схемы +4. Запустить тесты регистрации + +```bash +pytest tests/api/auth/test_auth_and_registration.py::test_register_success_sends_email_and_user_inactive -v +``` + +--- + +**Приоритет:** 🔴 КРИТИЧЕСКИЙ - блокирует основной функционал +**Трудозатраты:** ⚡ 5-10 минут на реализацию + 2-3 минуты на тестирование diff --git a/docs/reports/TESTS_REFACTORING_REPORT.md b/docs/reports/TESTS_REFACTORING_REPORT.md index d045cbb2..ea770cde 100644 --- a/docs/reports/TESTS_REFACTORING_REPORT.md +++ b/docs/reports/TESTS_REFACTORING_REPORT.md @@ -1,6 +1,6 @@ -# 🎉 Отчет о рефакторинге тестов +зне# 🎉 Отчет о рефакторинге тестов -**Дата:** 5 января 2026 +**Дата:** 5 января 2026 **Статус:** ЗАВЕРШЕНО ✅ ## 📊 Результаты рефакторинга @@ -125,5 +125,5 @@ pytest -m integration - ✅ Manual tests правильно изолированы - ✅ Конфигурация оптимизирована -**Наше исправление (коммит `de0caea`):** БЕЗОПАСНО ✅ +**Наше исправление (коммит `de0caea`):** БЕЗОПАСНО ✅ **Готовность к production:** ДА 🚀