From d5b5437c6056bf04b4178560e87efafa84b5774f 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:47:56 +0300 Subject: [PATCH] fix: password reset emails now use HTML template for Gmail compatibility --- backend/auth_test_results.txt | 267 ++++++++++++++++++ backend/employees/views_auth.py | 1 + .../emails/password_reset_email.html | 15 +- 3 files changed, 279 insertions(+), 4 deletions(-) create mode 100644 backend/auth_test_results.txt diff --git a/backend/auth_test_results.txt b/backend/auth_test_results.txt new file mode 100644 index 00000000..ac67f50b --- /dev/null +++ b/backend/auth_test_results.txt @@ -0,0 +1,267 @@ +============================= test session starts ============================= +platform win32 -- Python 3.12.10, pytest-8.4.1, pluggy-1.6.0 -- C:\Users\nadeini\Dev\EUSRR\.venv\Scripts\python.exe +cachedir: .pytest_cache +django: version: 5.2.4, settings: eusrr_backend.settings (from ini) +rootdir: C:\Users\nadeini\Dev\EUSRR\backend +configfile: pytest.ini +plugins: anyio-4.12.0, django-4.11.1 +collecting ... collected 19 items + +tests/api/auth/test_auth_and_registration.py::test_register_success_sends_email_and_user_inactive FAILED [ 5%] +tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[first_name] PASSED [ 10%] +tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[last_name] PASSED [ 15%] +tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[phone_number] PASSED [ 21%] +tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[email] PASSED [ 26%] +tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[password] PASSED [ 31%] +tests/api/auth/test_auth_and_registration.py::test_register_missing_required_field[birth_date] PASSED [ 36%] +tests/api/auth/test_auth_and_registration.py::test_register_requires_at_least_one_contact PASSED [ 42%] +tests/api/auth/test_auth_and_registration.py::test_register_birth_date_must_be_valid_date PASSED [ 47%] +tests/api/auth/test_auth_and_registration.py::test_register_duplicate_email_phone FAILED [ 52%] +tests/api/auth/test_auth_and_registration.py::test_register_accepts_optional_fields FAILED [ 57%] +tests/api/auth/test_auth_and_registration.py::test_verify_email_success_activates_user FAILED [ 63%] +tests/api/auth/test_auth_and_registration.py::test_verify_email_wrong_code FAILED [ 68%] +tests/api/auth/test_auth_and_registration.py::test_resend_email_sends_new_code_and_old_becomes_invalid FAILED [ 73%] +tests/api/auth/test_auth_and_registration.py::test_verify_expired_more_than_5_minutes_deletes_account FAILED [ 78%] +tests/api/auth/test_auth_and_registration.py::test_login_by_email_denied_before_verify FAILED [ 84%] +tests/api/auth/test_auth_and_registration.py::test_login_by_email_allowed_after_verify FAILED [ 89%] +tests/api/auth/test_auth_and_registration.py::test_login_by_phone_allowed_after_verify FAILED [ 94%] +tests/api/auth/test_auth_and_registration.py::test_login_wrong_password FAILED [100%] + +================================== FAILURES =================================== +_____________ test_register_success_sends_email_and_user_inactive _____________ + +api = + + def test_register_success_sends_email_and_user_inactive(api): + resp = register(api) +> assert resp.status_code == status.HTTP_201_CREATED +E assert 400 == 201 +E + where 400 = .status_code +E + and 201 = status.HTTP_201_CREATED + +tests\api\auth\test_auth_and_registration.py:102: AssertionError +---------------------------- Captured stderr setup ---------------------------- +Creating test database for alias 'default'... +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-01-05 07:59:47,115 django.request:253 Bad Request: /api/v1/auth/register/ +------------------------------ Captured log call ------------------------------ +WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'} +WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json +WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]} +_____________________ test_register_duplicate_email_phone _____________________ + +api = + + def test_register_duplicate_email_phone(api): + # 1) Ðåãèñòðèðóåì ïåðâîãî ïîëüçîâàòåëÿ (ïîêà íå ïîäòâåðæä¸í) + payload = register_payload() +> assert register(api, **payload).status_code == status.HTTP_201_CREATED +E assert 400 == 201 +E + where 400 = .status_code +E + where = register(, **{'birth_date': '1990-01-01', 'email': 'ivan@example.com', 'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', ...}) +E + and 201 = status.HTTP_201_CREATED + +tests\api\auth\test_auth_and_registration.py:149: AssertionError +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-01-05 07:59:47,550 django.request:253 Bad Request: /api/v1/auth/register/ +------------------------------ Captured log call ------------------------------ +WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'} +WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json +WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]} +____________________ test_register_accepts_optional_fields ____________________ + +api = + + def test_register_accepts_optional_fields(api): + resp = register( + api, + patronymic="Ïåòðîâè÷", + gender=1, + whatsapp="+79995550011", # ìîæíî óêàçàòü äðóãîé êîíòàêò + telegram="", # òåëåãðàì ìîæíî íå óêàçûâàòü, ò.ê. whatsapp åñòü + ) +> assert resp.status_code == status.HTTP_201_CREATED +E assert 400 == 201 +E + where 400 = .status_code +E + and 201 = status.HTTP_201_CREATED + +tests\api\auth\test_auth_and_registration.py:183: AssertionError +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-01-05 07:59:47,563 django.request:253 Bad Request: /api/v1/auth/register/ +------------------------------ Captured log call ------------------------------ +WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '', 'patronymic': 'Ïåòðîâè÷', 'gender': 1, 'whatsapp': '+79995550011'} +WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json +WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')]} +__________________ test_verify_email_success_activates_user ___________________ + +api = + + def test_verify_email_success_activates_user(api): +> assert register(api).status_code == status.HTTP_201_CREATED +E assert 400 == 201 +E + where 400 = .status_code +E + where = register() +E + and 201 = status.HTTP_201_CREATED + +tests\api\auth\test_auth_and_registration.py:190: AssertionError +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-01-05 07:59:47,574 django.request:253 Bad Request: /api/v1/auth/register/ +------------------------------ Captured log call ------------------------------ +WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'} +WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json +WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]} +________________________ test_verify_email_wrong_code _________________________ + +api = + + def test_verify_email_wrong_code(api): +> assert register(api).status_code == status.HTTP_201_CREATED +E assert 400 == 201 +E + where 400 = .status_code +E + where = register() +E + and 201 = status.HTTP_201_CREATED + +tests\api\auth\test_auth_and_registration.py:202: AssertionError +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-01-05 07:59:47,586 django.request:253 Bad Request: /api/v1/auth/register/ +------------------------------ Captured log call ------------------------------ +WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'} +WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json +WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]} +__________ test_resend_email_sends_new_code_and_old_becomes_invalid ___________ + +api = + + def test_resend_email_sends_new_code_and_old_becomes_invalid(api): +> assert register(api).status_code == status.HTTP_201_CREATED +E assert 400 == 201 +E + where 400 = .status_code +E + where = register() +E + and 201 = status.HTTP_201_CREATED + +tests\api\auth\test_auth_and_registration.py:212: AssertionError +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-01-05 07:59:47,598 django.request:253 Bad Request: /api/v1/auth/register/ +------------------------------ Captured log call ------------------------------ +WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'} +WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json +WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]} +___________ test_verify_expired_more_than_5_minutes_deletes_account ___________ + +api = +settings = + + def test_verify_expired_more_than_5_minutes_deletes_account(api, settings): + # Ðåãèñòðèðóåì +> assert register(api).status_code == status.HTTP_201_CREATED +E assert 400 == 201 +E + where 400 = .status_code +E + where = register() +E + and 201 = status.HTTP_201_CREATED + +tests\api\auth\test_auth_and_registration.py:239: AssertionError +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-01-05 07:59:47,610 django.request:253 Bad Request: /api/v1/auth/register/ +------------------------------ Captured log call ------------------------------ +WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'} +WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json +WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]} +__________________ test_login_by_email_denied_before_verify ___________________ + +api = + + def test_login_by_email_denied_before_verify(api): +> assert register(api).status_code == status.HTTP_201_CREATED +E assert 400 == 201 +E + where 400 = .status_code +E + where = register() +E + and 201 = status.HTTP_201_CREATED + +tests\api\auth\test_auth_and_registration.py:258: AssertionError +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-01-05 07:59:47,622 django.request:253 Bad Request: /api/v1/auth/register/ +------------------------------ Captured log call ------------------------------ +WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'} +WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json +WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]} +__________________ test_login_by_email_allowed_after_verify ___________________ + +api = + + def test_login_by_email_allowed_after_verify(api): +> assert register(api).status_code == status.HTTP_201_CREATED +E assert 400 == 201 +E + where 400 = .status_code +E + where = register() +E + and 201 = status.HTTP_201_CREATED + +tests\api\auth\test_auth_and_registration.py:265: AssertionError +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-01-05 07:59:47,634 django.request:253 Bad Request: /api/v1/auth/register/ +------------------------------ Captured log call ------------------------------ +WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'} +WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json +WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]} +__________________ test_login_by_phone_allowed_after_verify ___________________ + +api = + + def test_login_by_phone_allowed_after_verify(api): + # Ðåãèñòðèðóåì ñ òåëåôîíîì è ïîäòâåðæäàåì + payload = register_payload() +> assert register(api, **payload).status_code == status.HTTP_201_CREATED +E assert 400 == 201 +E + where 400 = .status_code +E + where = register(, **{'birth_date': '1990-01-01', 'email': 'ivan@example.com', 'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', ...}) +E + and 201 = status.HTTP_201_CREATED + +tests\api\auth\test_auth_and_registration.py:278: AssertionError +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-01-05 07:59:47,647 django.request:253 Bad Request: /api/v1/auth/register/ +------------------------------ Captured log call ------------------------------ +WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'} +WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json +WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]} +__________________________ test_login_wrong_password __________________________ + +api = + + def test_login_wrong_password(api): +> assert register(api).status_code == status.HTTP_201_CREATED +E assert 400 == 201 +E + where 400 = .status_code +E + where = register() +E + and 201 = status.HTTP_201_CREATED + +tests\api\auth\test_auth_and_registration.py:290: AssertionError +---------------------------- Captured stderr call ----------------------------- +[WARNING] 2026-01-05 07:59:47,659 django.request:253 Bad Request: /api/v1/auth/register/ +------------------------------ Captured log call ------------------------------ +WARNING api.v1.employees.views:views.py:368 [REGISTER] Received data: {'first_name': 'Èâàí', 'last_name': 'Èâàíîâ', 'phone_number': '+79990000001', 'email': 'ivan@example.com', 'password': 'Str0ngPass!', 'birth_date': '1990-01-01', 'telegram': '@ivan'} +WARNING api.v1.employees.views:views.py:369 [REGISTER] Content-Type: application/json +WARNING api.v1.employees.views:views.py:387 [REGISTER] Validation errors: {'avatar': [ErrorDetail(string='Íè îäíîãî ôàéëà íå áûëî îòïðàâëåíî.', code='required')], 'gender': [ErrorDetail(string='Ïîëå "Ïîë" îáÿçàòåëüíî äëÿ çàïîëíåíèÿ.', code='required')]} +-------------------------- Captured stderr teardown --------------------------- +Destroying test database for alias 'default'... +============================== warnings summary =============================== +tests/api/auth/test_auth_and_registration.py::test_register_success_sends_email_and_user_inactive + C:\Users\nadeini\Dev\EUSRR\.venv\Lib\site-packages\pyasn1\codec\ber\encoder.py:952: DeprecationWarning: tagMap is deprecated. Please use TAG_MAP instead. + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning) + +tests/api/auth/test_auth_and_registration.py::test_register_success_sends_email_and_user_inactive + C:\Users\nadeini\Dev\EUSRR\.venv\Lib\site-packages\pyasn1\codec\ber\encoder.py:952: DeprecationWarning: typeMap is deprecated. Please use TYPE_MAP instead. + warnings.warn(f"{attr} is deprecated. Please use {newAttr} instead.", DeprecationWarning) + +-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html +=========================== short test summary info =========================== +FAILED tests/api/auth/test_auth_and_registration.py::test_register_success_sends_email_and_user_inactive +FAILED tests/api/auth/test_auth_and_registration.py::test_register_duplicate_email_phone +FAILED tests/api/auth/test_auth_and_registration.py::test_register_accepts_optional_fields +FAILED tests/api/auth/test_auth_and_registration.py::test_verify_email_success_activates_user +FAILED tests/api/auth/test_auth_and_registration.py::test_verify_email_wrong_code +FAILED tests/api/auth/test_auth_and_registration.py::test_resend_email_sends_new_code_and_old_becomes_invalid +FAILED tests/api/auth/test_auth_and_registration.py::test_verify_expired_more_than_5_minutes_deletes_account +FAILED tests/api/auth/test_auth_and_registration.py::test_login_by_email_denied_before_verify +FAILED tests/api/auth/test_auth_and_registration.py::test_login_by_email_allowed_after_verify +FAILED tests/api/auth/test_auth_and_registration.py::test_login_by_phone_allowed_after_verify +FAILED tests/api/auth/test_auth_and_registration.py::test_login_wrong_password +================== 11 failed, 8 passed, 2 warnings in 7.69s =================== diff --git a/backend/employees/views_auth.py b/backend/employees/views_auth.py index 9e530d10..001d83d1 100644 --- a/backend/employees/views_auth.py +++ b/backend/employees/views_auth.py @@ -217,6 +217,7 @@ class ConfirmLogoutView(LoginRequiredMixin, View): class PasswordResetView(DjangoPasswordResetView): template_name = "auth/password_reset.html" email_template_name = "auth/password_reset_email.txt" + html_email_template_name = "emails/password_reset_email.html" subject_template_name = "auth/password_reset_subject.txt" success_url = "/auth/password-reset/done/" diff --git a/backend/templates/emails/password_reset_email.html b/backend/templates/emails/password_reset_email.html index a1c23b4e..8c860d29 100644 --- a/backend/templates/emails/password_reset_email.html +++ b/backend/templates/emails/password_reset_email.html @@ -2,11 +2,18 @@

Сбро� парол�

-

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

-

Перейдите по ��ылке дл� �мены парол�:

-
- {{ protocol }}://{{ domain }}{% url 'auth_front:password_reset_confirm' uidb64=uid token=token %} +

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

+

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

+ +

Или �копируйте ��ылку в браузер:

+

+ {{ protocol }}://{{ domain }}{% url 'auth_front:password_reset_confirm' uidb64=uid token=token %} +


Е�ли вы не запрашивали �бро� парол�, про�то игнорируйте �то пи�ьмо.