Skip to content

Commit e71527b

Browse files
authored
Merge pull request #650 from PROCOLLAB-github/feature/configurable-email-settings
Make email settings configurable
2 parents 2c6e31a + 4b5606c commit e71527b

5 files changed

Lines changed: 86 additions & 11 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ DEBUG=
22
DJANGO_SECRET_KEY=
33

44
EMAIL_USER=
5+
EMAIL_BACKEND=anymail.backends.unisender_go.EmailBackend
6+
VERIFY_EMAIL_REDIRECT_URL=https://app.procollab.ru/auth/verification/
57
EMAIL_PASSWORD=
68
EMAIL_HOST=
79
EMAIL_PORT=

procollab/settings.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,17 @@
318318
SESSION_COOKIE_SECURE = not DEBUG
319319
CSRF_COOKIE_SECURE = not DEBUG
320320

321-
EMAIL_BACKEND = "anymail.backends.unisender_go.EmailBackend"
321+
EMAIL_BACKEND = config(
322+
"EMAIL_BACKEND",
323+
default="anymail.backends.unisender_go.EmailBackend",
324+
cast=str,
325+
)
326+
327+
VERIFY_EMAIL_REDIRECT_URL = config(
328+
"VERIFY_EMAIL_REDIRECT_URL",
329+
default="https://app.procollab.ru/auth/verification/",
330+
cast=str,
331+
)
322332

323333
UNISENDER_GO_API_KEY = config("UNISENDER_GO_API_KEY", default="", cast=str)
324334
ANYMAIL = {

users/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class OnboardingStage(Enum):
2929
(INVESTOR, "Инвестор"),
3030
)
3131

32-
VERIFY_EMAIL_REDIRECT_URL = "https://app.procollab.ru/auth/verification/"
32+
VERIFY_EMAIL_REDIRECT_URL = settings.VERIFY_EMAIL_REDIRECT_URL
3333

3434

3535
PROTOCOL = "https"

users/tests/test_auth_api.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
import runpy
2+
from pathlib import Path
13
from unittest.mock import patch
4+
from urllib.parse import urlparse
25

3-
from django.test import TestCase
6+
from django.conf import settings
7+
from django.test import SimpleTestCase, TestCase, override_settings
48
from django.urls import reverse
59
from rest_framework.test import APIClient
10+
from rest_framework_simplejwt.tokens import RefreshToken
611

712
from tests.constants import USER_CREATE_DATA
813
from users.models import CustomUser
@@ -48,6 +53,21 @@ def test_user_registration_rejects_invalid_payload(self):
4853

4954
self.assertEqual(response.status_code, 400)
5055

56+
@override_settings(EMAIL_BACKEND="django.core.mail.backends.console.EmailBackend")
57+
@patch(
58+
"django.core.mail.backends.console.EmailBackend.write_message",
59+
return_value=1,
60+
)
61+
def test_user_registration_works_with_console_email_backend(self, write_message_mock):
62+
response = self.client.post("/auth/users/", USER_CREATE_DATA, format="json")
63+
64+
self.assertEqual(response.status_code, 201)
65+
self.assertEqual(response.data["email"], USER_CREATE_DATA["email"])
66+
self.assertEqual(response.data["is_active"], False)
67+
user = CustomUser.objects.get(email=USER_CREATE_DATA["email"])
68+
self.assertFalse(user.is_active)
69+
write_message_mock.assert_called_once()
70+
5171
def test_token_obtain_pair_updates_last_login(self):
5272
user = build_user(email="login@example.com")
5373

@@ -83,3 +103,49 @@ def test_removed_legacy_routes_return_404(self):
83103
self.assertEqual(self.client.get("/auth/users/clone-data").status_code, 404)
84104
self.assertEqual(self.client.get("/auth/subscription/").status_code, 404)
85105
self.assertEqual(self.client.post("/auth/subscription/buy/").status_code, 404)
106+
107+
108+
class EmailSettingsDefaultsTests(SimpleTestCase):
109+
def test_email_settings_keep_existing_defaults_without_env(self):
110+
settings_path = Path(__file__).resolve().parents[2] / "procollab/settings.py"
111+
112+
def return_default(_name, default=None, cast=None):
113+
return cast(default) if cast else default
114+
115+
with patch("decouple.config", side_effect=return_default):
116+
loaded_settings = runpy.run_path(settings_path)
117+
118+
self.assertEqual(
119+
loaded_settings["EMAIL_BACKEND"],
120+
"anymail.backends.unisender_go.EmailBackend",
121+
)
122+
self.assertEqual(
123+
loaded_settings["VERIFY_EMAIL_REDIRECT_URL"],
124+
"https://app.procollab.ru/auth/verification/",
125+
)
126+
127+
128+
class VerifyEmailRedirectTests(TestCase):
129+
def setUp(self):
130+
self.client = APIClient()
131+
132+
@override_settings(
133+
VERIFY_EMAIL_REDIRECT_URL=("https://react-dev.procollab.ru/auth/verification")
134+
)
135+
def test_confirmation_handler_uses_configured_redirect(self):
136+
user = build_user(email="confirm@example.com", is_active=False)
137+
token = str(RefreshToken.for_user(user).access_token)
138+
139+
response = self.client.get(
140+
"/auth/account-confirm-email/",
141+
{"token": token},
142+
)
143+
144+
self.assertEqual(response.status_code, 302)
145+
redirect = urlparse(response["Location"])
146+
self.assertEqual(
147+
redirect._replace(query="", fragment="").geturl(),
148+
settings.VERIFY_EMAIL_REDIRECT_URL,
149+
)
150+
self.assertTrue(redirect.query.startswith("access_token="))
151+
self.assertIn("&refresh_token=", redirect.query)

users/views.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,7 @@
4141
)
4242
from projects.pagination import ProjectsPagination
4343
from projects.serializers import ProjectListSerializer
44-
from users.constants import (
45-
VERBOSE_ROLE_TYPES,
46-
VERBOSE_USER_TYPES,
47-
VERIFY_EMAIL_REDIRECT_URL,
48-
)
44+
from users.constants import VERBOSE_ROLE_TYPES, VERBOSE_USER_TYPES
4945
from users.helpers import check_related_fields_update, force_verify_user, verify_email
5046
from users.models import LikesOnProject, UserAchievement, UserSkillConfirmation
5147
from users.permissions import IsAchievementOwnerOrReadOnly
@@ -276,6 +272,7 @@ class VerifyEmail(GenericAPIView):
276272

277273
def get(self, request):
278274
token = request.GET.get("token")
275+
redirect_url = settings.VERIFY_EMAIL_REDIRECT_URL
279276

280277
try:
281278
payload = jwt.decode(jwt=token, key=settings.SECRET_KEY, algorithms=["HS256"])
@@ -288,20 +285,20 @@ def get(self, request):
288285
user.save()
289286

290287
return redirect(
291-
f"{VERIFY_EMAIL_REDIRECT_URL}?access_token={access_token}&refresh_token={refresh_token}",
288+
f"{redirect_url}?access_token={access_token}&refresh_token={refresh_token}",
292289
status=status.HTTP_200_OK,
293290
message="Succeed",
294291
)
295292

296293
except jwt.ExpiredSignatureError:
297294
return redirect(
298-
VERIFY_EMAIL_REDIRECT_URL,
295+
redirect_url,
299296
status=status.HTTP_400_BAD_REQUEST,
300297
message="Activate Expired",
301298
)
302299
except jwt.DecodeError:
303300
return redirect(
304-
VERIFY_EMAIL_REDIRECT_URL,
301+
redirect_url,
305302
status=status.HTTP_400_BAD_REQUEST,
306303
message="Decode error",
307304
)

0 commit comments

Comments
 (0)