From 4b5606caaf209469e8bd14463a1d186d095f952b Mon Sep 17 00:00:00 2001 From: Fedor Date: Fri, 17 Jul 2026 01:01:22 +0300 Subject: [PATCH] Make email settings configurable --- .env.example | 2 ++ procollab/settings.py | 12 ++++++- users/constants.py | 2 +- users/tests/test_auth_api.py | 68 +++++++++++++++++++++++++++++++++++- users/views.py | 13 +++---- 5 files changed, 86 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index 13e8e71d..cb0067a8 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,8 @@ DEBUG= DJANGO_SECRET_KEY= EMAIL_USER= +EMAIL_BACKEND=anymail.backends.unisender_go.EmailBackend +VERIFY_EMAIL_REDIRECT_URL=https://app.procollab.ru/auth/verification/ EMAIL_PASSWORD= EMAIL_HOST= EMAIL_PORT= diff --git a/procollab/settings.py b/procollab/settings.py index 1b1ed91e..b1d1ca48 100644 --- a/procollab/settings.py +++ b/procollab/settings.py @@ -318,7 +318,17 @@ SESSION_COOKIE_SECURE = not DEBUG CSRF_COOKIE_SECURE = not DEBUG -EMAIL_BACKEND = "anymail.backends.unisender_go.EmailBackend" +EMAIL_BACKEND = config( + "EMAIL_BACKEND", + default="anymail.backends.unisender_go.EmailBackend", + cast=str, +) + +VERIFY_EMAIL_REDIRECT_URL = config( + "VERIFY_EMAIL_REDIRECT_URL", + default="https://app.procollab.ru/auth/verification/", + cast=str, +) UNISENDER_GO_API_KEY = config("UNISENDER_GO_API_KEY", default="", cast=str) ANYMAIL = { diff --git a/users/constants.py b/users/constants.py index 4ba1bb46..913f9a63 100644 --- a/users/constants.py +++ b/users/constants.py @@ -29,7 +29,7 @@ class OnboardingStage(Enum): (INVESTOR, "Инвестор"), ) -VERIFY_EMAIL_REDIRECT_URL = "https://app.procollab.ru/auth/verification/" +VERIFY_EMAIL_REDIRECT_URL = settings.VERIFY_EMAIL_REDIRECT_URL PROTOCOL = "https" diff --git a/users/tests/test_auth_api.py b/users/tests/test_auth_api.py index 4cff999b..34425044 100644 --- a/users/tests/test_auth_api.py +++ b/users/tests/test_auth_api.py @@ -1,8 +1,13 @@ +import runpy +from pathlib import Path from unittest.mock import patch +from urllib.parse import urlparse -from django.test import TestCase +from django.conf import settings +from django.test import SimpleTestCase, TestCase, override_settings from django.urls import reverse from rest_framework.test import APIClient +from rest_framework_simplejwt.tokens import RefreshToken from tests.constants import USER_CREATE_DATA from users.models import CustomUser @@ -48,6 +53,21 @@ def test_user_registration_rejects_invalid_payload(self): self.assertEqual(response.status_code, 400) + @override_settings(EMAIL_BACKEND="django.core.mail.backends.console.EmailBackend") + @patch( + "django.core.mail.backends.console.EmailBackend.write_message", + return_value=1, + ) + def test_user_registration_works_with_console_email_backend(self, write_message_mock): + response = self.client.post("/auth/users/", USER_CREATE_DATA, format="json") + + self.assertEqual(response.status_code, 201) + self.assertEqual(response.data["email"], USER_CREATE_DATA["email"]) + self.assertEqual(response.data["is_active"], False) + user = CustomUser.objects.get(email=USER_CREATE_DATA["email"]) + self.assertFalse(user.is_active) + write_message_mock.assert_called_once() + def test_token_obtain_pair_updates_last_login(self): user = build_user(email="login@example.com") @@ -83,3 +103,49 @@ def test_removed_legacy_routes_return_404(self): self.assertEqual(self.client.get("/auth/users/clone-data").status_code, 404) self.assertEqual(self.client.get("/auth/subscription/").status_code, 404) self.assertEqual(self.client.post("/auth/subscription/buy/").status_code, 404) + + +class EmailSettingsDefaultsTests(SimpleTestCase): + def test_email_settings_keep_existing_defaults_without_env(self): + settings_path = Path(__file__).resolve().parents[2] / "procollab/settings.py" + + def return_default(_name, default=None, cast=None): + return cast(default) if cast else default + + with patch("decouple.config", side_effect=return_default): + loaded_settings = runpy.run_path(settings_path) + + self.assertEqual( + loaded_settings["EMAIL_BACKEND"], + "anymail.backends.unisender_go.EmailBackend", + ) + self.assertEqual( + loaded_settings["VERIFY_EMAIL_REDIRECT_URL"], + "https://app.procollab.ru/auth/verification/", + ) + + +class VerifyEmailRedirectTests(TestCase): + def setUp(self): + self.client = APIClient() + + @override_settings( + VERIFY_EMAIL_REDIRECT_URL=("https://react-dev.procollab.ru/auth/verification") + ) + def test_confirmation_handler_uses_configured_redirect(self): + user = build_user(email="confirm@example.com", is_active=False) + token = str(RefreshToken.for_user(user).access_token) + + response = self.client.get( + "/auth/account-confirm-email/", + {"token": token}, + ) + + self.assertEqual(response.status_code, 302) + redirect = urlparse(response["Location"]) + self.assertEqual( + redirect._replace(query="", fragment="").geturl(), + settings.VERIFY_EMAIL_REDIRECT_URL, + ) + self.assertTrue(redirect.query.startswith("access_token=")) + self.assertIn("&refresh_token=", redirect.query) diff --git a/users/views.py b/users/views.py index 9300d2cd..74b08b54 100644 --- a/users/views.py +++ b/users/views.py @@ -41,11 +41,7 @@ ) from projects.pagination import ProjectsPagination from projects.serializers import ProjectListSerializer -from users.constants import ( - VERBOSE_ROLE_TYPES, - VERBOSE_USER_TYPES, - VERIFY_EMAIL_REDIRECT_URL, -) +from users.constants import VERBOSE_ROLE_TYPES, VERBOSE_USER_TYPES from users.helpers import check_related_fields_update, force_verify_user, verify_email from users.models import LikesOnProject, UserAchievement, UserSkillConfirmation from users.permissions import IsAchievementOwnerOrReadOnly @@ -276,6 +272,7 @@ class VerifyEmail(GenericAPIView): def get(self, request): token = request.GET.get("token") + redirect_url = settings.VERIFY_EMAIL_REDIRECT_URL try: payload = jwt.decode(jwt=token, key=settings.SECRET_KEY, algorithms=["HS256"]) @@ -288,20 +285,20 @@ def get(self, request): user.save() return redirect( - f"{VERIFY_EMAIL_REDIRECT_URL}?access_token={access_token}&refresh_token={refresh_token}", + f"{redirect_url}?access_token={access_token}&refresh_token={refresh_token}", status=status.HTTP_200_OK, message="Succeed", ) except jwt.ExpiredSignatureError: return redirect( - VERIFY_EMAIL_REDIRECT_URL, + redirect_url, status=status.HTTP_400_BAD_REQUEST, message="Activate Expired", ) except jwt.DecodeError: return redirect( - VERIFY_EMAIL_REDIRECT_URL, + redirect_url, status=status.HTTP_400_BAD_REQUEST, message="Decode error", )