Dj-rest-auth + allauth не отправляет электронное письмо

Контекст: Я устанавливаю DRF + dj-rest-auth + allauth + simple-jwt для аутентификации пользователя.

Желаемое поведение: Зарегистрируйтесь без имени пользователя, только с электронной почтой. Авторизуйтесь, только если электронная почта подтверждена ссылкой, отправленной на электронную почту. Необходимо добавить логин в социальной сети.

Проблема: Кажется, что электронное письмо с подтверждением не отправляется. Когда я запускаю следующий тест, я вижу, что он хотел отправить какое-то электронное письмо, но оно нигде не найдено.

Тестовый код:

    client = APIClient()
    url = reverse("rest_register")  # dj-rest-auth register endpoint
    # Register a user
    data = {
        "email": "user1@example.com",
        "password1": "StrongPass123!",
        "password2": "StrongPass123!",
    }
    response = client.post(url, data, format="json")
    assert response.status_code == 201, response.data

    print(response.data)


    # Manually verify the user
    from allauth.account.models import EmailConfirmation

    user = User.objects.get(email="user1@example.com")
    from django.core import mail    
    print(f'Amount of sent emails: {len(mail.outbox)}')
    print(f'Email Confimation exists: {EmailConfirmation.objects.filter(email_address__email=user.email).exists()}')

Это выводит:

{'detail': 'Verification e-mail sent.'}
Amount of sent emails: 0
Email Confimation exists: False

Мой код:

core/urls.py

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('api/auth/', include('authentication.urls')),
    path("admin/", admin.site.urls),

    path("accounts/", include("allauth.urls")),
]

authentication/urls.py

from dj_rest_auth.jwt_auth import get_refresh_view
from dj_rest_auth.registration.views import RegisterView, VerifyEmailView
from dj_rest_auth.views import LoginView, LogoutView, UserDetailsView
from django.urls import path
from rest_framework_simplejwt.views import TokenVerifyView

urlpatterns = [
    path("register/", RegisterView.as_view(), name="rest_register"),
    path("register/verify-email/", VerifyEmailView.as_view(), name="account_email_verification_sent"), # Docs says I must add if email confirmation is mandatory

    path("login/", LoginView.as_view(), name="rest_login"),
    path("logout/", LogoutView.as_view(), name="rest_logout"),

    path("user/", UserDetailsView.as_view(), name="rest_user_details"),

    path("token/verify/", TokenVerifyView.as_view(), name="token_verify"),
    path("token/refresh/", get_refresh_view().as_view(), name="token_refresh"),
]

Пользовательская модель пользователя:

from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, BaseUserManager
from django.db import models


class UserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError("The Email field must be set")
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password=None, **extra_fields):
        extra_fields.setdefault("is_staff", True)
        extra_fields.setdefault("is_superuser", True)
        return self.create_user(email, password, **extra_fields)

class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)
    is_active = models.BooleanField(default=True) # Some SO question fixed this setting is_active default value to True
    is_staff = models.BooleanField(default=False)

    USERNAME_FIELD = "email"
    REQUIRED_FIELDS = []

    objects = UserManager()

Соответствующие настройки:

SITE_ID = 1
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    "django.contrib.sites",
    'django.contrib.staticfiles',    

    # Django REST Framework
    "rest_framework",
    "rest_framework.authtoken",
    # Simple JWT
    "rest_framework_simplejwt",

    # Allauth
    "allauth",
    "allauth.account",
    "allauth.socialaccount",

    # dj-rest-auth
    "dj_rest_auth",
    "dj_rest_auth.registration",

    # CORS
    "corsheaders",

    # Local apps
    "authentication.apps.AuthenticationConfig",
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',

    # Allauth
    'allauth.account.middleware.AccountMiddleware',

    # CORS
    "corsheaders.middleware.CorsMiddleware",
    "django.middleware.common.CommonMiddleware",
]

ROOT_URLCONF = 'core.urls'

EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" # for testing

DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL  = "authentication.User"

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=7),
    "ROTATE_REFRESH_TOKENS": False,
    "BLACKLIST_AFTER_ROTATION": False,
    "UPDATE_LAST_LOGIN": True,
    "SIGNING_KEY": "complexsigningkey",  # generate a key and replace me
    "ALGORITHM": "HS512",
}

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ]
}

# Dj-rest-auth settings
REST_AUTH = {
    "USE_JWT": True,
    "JWT_AUTH_HTTPONLY": False,
    "REGISTER_SERIALIZER": "authentication.serializers.RegisterSerializer"
}

# Allauth settings
ACCOUNT_SIGNUP_FIELDS = ['email*', 'password1*', 'password2*']
ACCOUNT_LOGIN_METHODS = {'email'}
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_AUTHENTICATION_METHOD = "email"
ACCOUNT_EMAIL_CONFIRMATION_HMAC = True # This is the default. But just to be explicit.

AUTHENTICATION_BACKENDS = [
    "django.contrib.auth.backends.ModelBackend",
    "allauth.account.auth_backends.AuthenticationBackend",
]

Проблема заключалась в том, что показанный тест был вторым тестом, который отправляет запрос на регистрацию с тем же адресом электронной почты. Это проблема, потому что несмотря на то, что база данных django очищается между тестами при использовании чего-то вроде @pytest.mark.django_db decorator, она не очищает кэш django. Allauth, прежде чем отправить электронное письмо с подтверждением, проверяет, не было ли недавно отправлено другое электронное письмо на этот адрес, и эта информация сохраняется в django.core.cache ConnectionProxy объекте.

Решением является использование cache.clear(), где кэш получается с помощью django.core.cache import cache . Вы можете написать эту строку в начале каждого теста, или, что еще лучше, добавить фиксатор в начале вашего тестового файла:

@pytest.fixture(autouse=True)
def clear_cache_before_each_test():
    """Fixture to clear the Django cache before each test."""
    cache.clear()
Вернуться на верх