我正在使用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的答案没有重现错误。
你的推理和代码看起来不错。
但是,您没有提供完整的代码,一定有您没有看到的错误。
可疑事实
- 当您未登录时,它不是默认的 302。 (
@login_required
等重定向,但您的代码没有( - 您的
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,这就是我被告知要做的。