You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DRF+pytest测试重置密码序列化器时出现KeyError: 'request'求助

解决Djoser序列化器测试中的KeyError: 'request'问题

问题说明

测试继承自Djoser PasswordRetypeSerializer 的 ResetPasswordConfirmSerializer 时触发 KeyError: 'request',原因是父类的验证逻辑尝试从 self.context["request"] 获取请求对象,但测试中实例化序列化器时未传入包含request的上下文,使用force_authenticate也无法解决该问题。

相关代码

models.py

class ResetPasswordCode(IsActive):
    """код для смены пароля"""
    user = models.ForeignKey('User', models.CASCADE, verbose_name='Пользователь')
    code = models.CharField('Код', max_length=12)
    expires_at = models.DateTimeField('Активен до')

    class Meta:
        verbose_name = 'Код сброса пароля'
        verbose_name_plural = 'Коды сброса пароля'


class ResetPasswordConfirmSerializer(PasswordRetypeSerializer):
    code = serializers.CharField()

    def validate_code(self, value):
        self.reset = ResetPasswordCode.objects.filter(code=value,
                                                      is_active=True,
                                                      expires_at__gt=timezone.now()).last()
        if not self.reset:
            raise serializers.ValidationError(ExceptionText.RESET_PASS_CODE_ERR)
        return value

    def validate(self, attrs):
        self.user = self.reset.user
        attrs = super(ResetPasswordConfirmSerializer, self).validate(attrs)
        return attrs

    def set_password(self):
        self.user.set_password(self.initial_data.get('new_password'))
        self.user.save()
        self.reset.is_active = False
        self.reset.save()

测试代码(原代码)

@pytest.mark.django_db
def test_reset_password_confirm_serializer():
        user = mixer.blend(User)
        reset_password_code = mixer.blend(ResetPasswordCode, user=user, expires_at=timezone.now() + timezone.timedelta(hours=1))

        data = {'new_password': 'yrplk123', 're_new_password': 'yrplk123', 'code': reset_password_code.code}
        serializer = ResetPasswordConfirmSerializer(data=data)
        result = serializer.is_valid()
        assert result == True
        assert serializer.reset == reset_password_code
        assert serializer.user == user

报错信息

pytest
=============================================================== test session starts ===============================================================
platform win32 -- Python 3.11.1, pytest-7.2.2, pluggy-1.0.0
django: settings: Alrent.settings (from ini)
rootdir: C:\Users\hp\Desktop\aitrix-alrent-ccfa942dfddd, configfile: pytest.ini
plugins: Faker-12.0.1, cov-4.0.0, django-4.5.2
collected 14 items

apps\users\tests.py .............F                                                                                                           [100%]

==================================================================== FAILURES =====================================================================
_____________________________________________________ test_reset_password_confirm_serializer ______________________________________________________ 

    @pytest.mark.django_db
    def test_reset_password_confirm_serializer():
            user = mixer.blend(User)
            reset_password_code = mixer.blend(ResetPasswordCode, user=user, expires_at=timezone.now() + timezone.timedelta(hours=1))

            data = {'new_password': 'yrplk123', 're_new_password': 'yrplk123', 'code': reset_password_code.code}
            serializer = ResetPasswordConfirmSerializer(data=data)
>           result = serializer.is_valid()

apps\users\tests.py:164:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
..\..\AppData\Local\pypoetry\Cache\virtualenvs\alrent-TmlPO8pf-py3.11\Lib\site-packages\rest_framework\serializers.py:227: in is_valid
    self._validated_data = self.run_validation(self.initial_data)
..\..\AppData\Local\pypoetry\Cache\virtualenvs\alrent-TmlPO8pf-py3.11\Lib\site-packages\rest_framework\serializers.py:429: in run_validation        
    value = self.validate(value)
apps\users\serializers.py:318: in validate
    attrs = super(ResetPasswordConfirmSerializer, self).validate(attrs)
..\..\AppData\Local\pypoetry\Cache\virtualenvs\alrent-TmlPO8pf-py3.11\Lib\site-packages\djoser\serializers.py:229: in validate
    attrs = super().validate(attrs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = ResetPasswordConfirmSerializer(data={'new_password': 'yrplk123', 're_new_password': 'yrplk123', 'code': 'MIEhWSRMsvLr'...le={'input_type': 'password'})
    re_new_password = CharField(style={'input_type': 'password'})
    code = CharField()
attrs = OrderedDict([('new_password', 'yrplk123'), ('re_new_password', 'yrplk123'), ('code', 'MIEhWSRMsvLr')])

    def validate(self, attrs):
>       user = self.context["request"].user or self.user
E       KeyError: 'request'

..\..\AppData\Local\pypoetry\Cache\virtualenvs\alrent-TmlPO8pf-py3.11\Lib\site-packages\djoser\serializers.py:210: KeyError
================================================================ warnings summary ================================================================= 
..\..\AppData\Local\pypoetry\Cache\virtualenvs\alrent-TmlPO8pf-py3.11\Lib\site-packages\coreapi\codecs\download.py:5
  C:\Users\hp\AppData\Local\pypoetry\Cache\virtualenvs\alrent-TmlPO8pf-py3.11\Lib\site-packages\coreapi\codecs\download.py:5: DeprecationWarning: 'cgi' is deprecated and slated for removal in Python 3.13
    import cgi

..\..\AppData\Local\pypoetry\Cache\virtualenvs\alrent-TmlPO8pf-py3.11\Lib\site-packages\kombu\utils\compat.py:82
  C:\Users\hp\AppData\Local\pypoetry\Cache\virtualenvs\alrent-TmlPO8pf-py3.11\Lib\site-packages\kombu\utils\compat.py:82: DeprecationWarning: SelectableGroups dict interface is deprecated. Use select.
    for ep in importlib_metadata.entry_points().get(namespace, [])

..\..\AppData\Local\pypoetry\Cache\virtualenvs\alrent-TmlPO8pf-py3.11\Lib\site-packages\rest_framework_simplejwt\utils.py:6
  C:\Users\hp\AppData\Local\pypoetry\Cache\virtualenvs\alrent-TmlPO8pf-py3.11\Lib\site-packages\rest_framework_simplejwt\utils.py:6: RemovedInDjango50Warning: The django.utils.timezone.utc alias is deprecated. Please update your code to use datetime.timezone.utc instead.
    from django.utils.timezone import is_naive, make_aware, utc

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
============================================================= short test summary info =============================================================
FAILED apps/users/tests.py::test_reset_password_confirm_serializer - KeyError: 'request'

解决方案

方案1:给序列化器传入包含request的上下文

在测试中构造一个HttpRequest对象,将其放入context参数传给序列化器,同时可以将用户赋值给request.user,满足父类验证逻辑的需求:

from django.http import HttpRequest
import pytest
from mixer.backend.django import mixer
from django.utils import timezone

@pytest.mark.django_db
def test_reset_password_confirm_serializer():
    user = mixer.blend(User)
    reset_password_code = mixer.blend(ResetPasswordCode, user=user, expires_at=timezone.now() + timezone.timedelta(hours=1))

    data = {'new_password': 'yrplk123', 're_new_password': 'yrplk123', 'code': reset_password_code.code}
    
    # 构造request对象并绑定用户
    request = HttpRequest()
    request.user = user
    
    # 传入包含request的上下文
    serializer = ResetPasswordConfirmSerializer(data=data, context={'request': request})
    result = serializer.is_valid()
    
    assert result == True
    assert serializer.reset == reset_password_code
    assert serializer.user == user

方案2:重写验证逻辑避免依赖request

由于你的序列化器已经通过验证码获取了用户self.user,可以跳过父类中依赖request的验证步骤,直接调用PasswordRetypeSerializer的验证逻辑(仅验证两次密码输入一致):

修改ResetPasswordConfirmSerializer的validate方法:

from djoser.serializers import PasswordRetypeSerializer

class ResetPasswordConfirmSerializer(PasswordRetypeSerializer):
    code = serializers.CharField()

    def validate_code(self, value):
        self.reset = ResetPasswordCode.objects.filter(code=value,
                                                      is_active=True,
                                                      expires_at__gt=timezone.now()).last()
        if not self.reset:
            raise serializers.ValidationError(ExceptionText.RESET_PASS_CODE_ERR)
        return value

    def validate(self, attrs):
        self.user = self.reset.user
        # 直接调用PasswordRetypeSerializer的validate,跳过上层依赖request的验证
        attrs = super(PasswordRetypeSerializer, self).validate(attrs)
        return attrs

    def set_password(self):
        self.user.set_password(self.initial_data.get('new_password'))
        self.user.save()
        self.reset.is_active = False
        self.reset.save()

这样父类的验证逻辑不会去读取self.context["request"],直接完成密码重复验证即可。

内容的提问来源于stack exchange,提问作者Bte Deni

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 02:19:53