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
相关产品推荐
相关产品推荐

