Django Rest 框架在单元测试中给出 302 当force_login() 在详细信息视图上时?



我正在使用Django Rest Framework来提供API。我有几个测试效果很好。要发布帖子,用户需要登录,我还对登录用户的详细信息视图进行了一些检查。我这样做如下:

class DeviceTestCase(APITestCase):
USERNAME = "username"
EMAIL = 'a@b.com'
PASSWORD = "password"
def setUp(self):
self.sa_group, _ = Group.objects.get_or_create(name=settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME)
self.authorized_user = User.objects.create_user(self.USERNAME, self.EMAIL, self.PASSWORD)
self.sa_group.user_set.add(self.authorized_user)
def test_post(self):
device = DeviceFactory.build()
url = reverse('device-list')
self.client.force_login(self.authorized_user)
response = self.client.post(url, data={'some': 'test', 'data': 'here'}, format='json')
self.client.logout()
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
# And some more tests here
def test_detail_logged_in(self):
device = DeviceFactory.create()
url = reverse('device-detail', kwargs={'pk': device.pk})
self.client.force_login(self.authorized_user)
response = self.client.get(url)
self.client.logout()
self.assertEqual(status.HTTP_200_OK, response.status_code, 'Wrong response code for {}'.format(url))
# And some more tests here

第一个测试效果很好。它发布新记录,所有检查都通过。不过,第二个测试失败了。它给出一个错误说

AssertionError: 200 != 302 : Wrong response code for /sa/devices/1/

事实证明,列表视图将用户重定向到登录屏幕。为什么第一个测试完美地记录了用户,但第二个测试是否将用户重定向到登录屏幕?我错过了什么吗?

以下是视图:

class APIAuthGroup(InAuthGroup):
"""
A permission to allow all GETS, but only allow a POST if a user is logged in,
and is a member of the slimme apparaten role inside keycloak.
"""
allowed_group_names = [settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME]
def has_permission(self, request, view):
return request.method in SAFE_METHODS 
or super(APIAuthGroup, self).has_permission(request, view)

class DevicesViewSet(DatapuntViewSetWritable):
"""
A view that will return the devices and makes it possible to post new ones
"""
queryset = Device.objects.all().order_by('id')
serializer_class = DeviceSerializer
serializer_detail_class = DeviceSerializer
http_method_names = ['post', 'list', 'get']
permission_classes = [APIAuthGroup]

这就是您收到此错误的原因。

依赖库

我按类名进行了一些搜索,以查找您正在使用的库,以便我可以在我的机器上重现问题。导致问题的库是名为keycloak_idc的库。该库安装了另一个库mozilla_django_oidc这将是您获得此库的原因。

为什么这个库会导致问题

在此库的README文件中,它为您提供了有关如何设置它的说明。这些都可以在此文件中找到。在这些说明中,它指示您添加AUTHENTICATION_BACKENDS

AUTHENTICATION_BACKENDS = [
'keycloak_oidc.auth.OIDCAuthenticationBackend',
...
]

添加此身份验证后端时,所有请求都将通过mozilla_django_oidc/middleware.py中定义的SessionRefresh类中定义的中间件。在此类中,始终调用方法process_request()

此方法要做的第一件事是调用is_refreshable_url()方法,如果请求方法是 POST,则始终返回 False。 否则(当请求方法是 GET 时(,它将返回 True。

现在,这个 if 条件的主体如下。

if not self.is_refreshable_url(request):
LOGGER.debug('request is not refreshable')
return
# lots of stuff in here
return HttpResponseRedirect(redirect_url)

由于这是一个中间件,如果请求是 POST 并且返回是 None,Django 将继续实际执行您的请求。但是,当请求为 GET 并且触发了行return HttpResponseRedirect(redirect_url)时,Django 甚至不会继续调用您的视图,并会立即返回 302 响应。

解决方案

经过几个小时的调试,我不知道这个中间件背后的确切逻辑,或者你到底想做什么来提供一个具体的解决方案,因为这一切都是基于猜测开始的,但一个天真的修复可能是你从设置文件中删除AUTHENTICATION_BACKENDS。虽然我觉得这是不可接受的,但也许你可以尝试使用另一个库来完成你正在尝试做的事情,或者找到另一种方法来做到这一点。另外,也许您可以联系作者,看看他们的想法。

所以我想你已经测试过了,你仍然得到相同的结果:

class APIAuthGroup(InAuthGroup):
def has_permission(self, request, view):
return True

为什么在第一次测试中使用DeviceFactory.build(),在第二次测试中使用DeviceFactory.create()

也许两者的合并可以帮助您:

def test_get(self):
device = DeviceFactory.build()
url = reverse('device-list')
response = self.client.get(url)
self.assertEqual(status.HTTP_200_OK, response.status_code)

这是setUp()方法的问题吗?据我所知,您可能正在为在第一次测试中已经创建的用户设置self.authorize_user。

相反,我会在每个测试中创建用户,确保用户不存在,如下所示:

user_exists = User.objects.filter(username=self.USERNAME, email=self.EMAIL).exists()
if not user_exists:
self.authorize_user = User.objects.create_user....

这可以解释为什么你的第一次测试通过了,为什么你的第二次测试没有通过,以及为什么@anupam-Chaplot的答案没有重现错误。

你的推理和代码看起来不错。

但是,您没有提供完整的代码,一定有您没有看到的错误。

可疑事实

  1. 当您未登录时,它不是默认的 302。 (@login_required等重定向,但您的代码没有(
  2. 您的APIAuthGroup权限确实允许非登录用户(return request.method in SAFE_METHODS(的GET请求,并且您正在使用GET请求(self.client.get(url)(

因此,这意味着您没有命中您认为正在命中的端点(您的 get 请求没有命中DevicesViewSet方法(

或者可能是您在settings.py中有一些global权限/重定向相关设置,这些设置可能与DRF相关。 例如:

REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}

url = reverse('device-detail', kwargs={'pk': device.pk}) 

可能不会指向您正在考虑的URL。 也许还有另一个网址 (/sa/devices/1/( 覆盖了视图集的网址。 (你可能有一个基于 django 视图的网址(

而且我没有解决为什么您在force_login之后被重定向.

如果确实是与登录相关的重定向,我能想到的就是self.authorized_user.refresh_from_db()或刷新request..

我想一些与登录相关的属性(例如会话或request.user(可能指向旧实例..(我没有证据或事实这可能会发生,但只是一种预感(,你最好不要为每个测试用例logging out/in(

你应该创建一个单独的设置文件进行测试,并添加到测试命令--settings=project_name.test_settings,这就是我被告知要做的。

相关内容

最新更新