我开始为我的端点“categories/”编写测试,但我无法通过已添加到正在测试的视图中的自定义权限。在此权限中,它会使用随 post 请求发送的有效负载中存在的“owner_id”字段来检查经过身份验证的用户的 ID。如果它们不匹配,则权限将拒绝对视图的访问。我编写此权限是为了阻止恶意请求(不是通过前端)向错误的用户添加类别。用户被视为使用 jwt 令牌进行了身份验证,该令牌是发布请求标头的一部分。理论上,Django 应该看到 jwt,从数据库获取经过身份验证的用户,然后在某个时候我的权限应该看到owner_id 字段等于检索到的用户的 id。也许我遗漏了创建测试用户和身份验证的工作原理?我可以使用 Postman 的经过身份验证的用户成功创建类别。如果您需要更多信息,请告诉我。
失败测试结果:
FAIL: test_create_category (categories.tests.test_categories_api.CategoryAPIViewTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/nwmus/units/backend/categories/tests/test_categories_api.py", line 35, in test_create_category
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
AssertionError: 403 != 201
测试:
class CategoryAPIViewTests(APITestCase):
categories_url = reverse("units_api:categories:category-list-create")
def setUp(self):
self.user = UnitsUser.objects.create_user(
email="[email protected]", username="testuser", password="testpassword"
)
self.access_token = RefreshToken.for_user(self.user).access_token
self.client.credentials(HTTP_AUTHORIZATION=f"Bearer {self.access_token}")
def test_create_category(self):
self.client.force_authenticate(user=self.user)
data = {
"name": "test category",
"description": "test description",
"owner_id": self.user.id,
}
logger.debug(f"user: {self.user}")
logger.debug(f"access_token: {self.access_token}")
response = self.client.post(self.categories_url, data=data)
logger.debug(f"data: {data}")
logger.debug(f"response.data: {response.data}")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["name"], data["name"])
self.assertEqual(response.data["description"], data["description"])
self.assertEqual(response.data["owner_id"], self.user.id)
我已经注销了一些我认为测试用例中必要的数据点。
DEBUG user: id: 1, username: testuser, email: [email protected]
DEBUG access_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzAyNjQ1NzczLCJpYXQiOjE3MDI2Mjc3NzMsImp0aSI6IjdjMWY5ZjdmYTI4NDRkMWJiMTEwYjg3ZjQxZjkxMDgzIiwidXNlcl9pZCI6MX0.7vwhvgRuclaVjJxWZ0nRLXgK4OI-fOg5G8AZwagFwY8
DEBUG data: {'name': 'test category', 'description': 'test description', 'owner_id': '1'}
DEBUG response.data: {'detail': ErrorDetail(string='owner_id field does not match authenticated user', code='permission_denied')}
解码后的access_token有效负载:
{
"token_type": "access",
"exp": 1702644373,
"iat": 1702626373,
"jti": "4f3fc61c5a174907923dd473dbb511d1",
"user_id": 1
}
如您所见,有效负载字段“user_id”、用户 ID 和数据字段“owner_id”全部匹配。我不知道从这里该去哪里。也许我的许可有缺陷,有更好的方法吗?
许可:
class UserIsOwnerPermission(permissions.BasePermission):
"""Permission that checks if authenticated user is the owner of the object being requested or created"""
def has_object_permission(self, request, view, obj):
return obj.owner_id == request.user
def has_permission(self, request, view):
# Catch POST requests that have an owner_id that does not match the authenticated user
if "owner_id" in request.data:
self.message = "owner_id field does not match authenticated user"
return request.user.id == request.data["owner_id"]
return True
查看:
class CategoryListCreateView(UserIsOwnerMixin, generics.ListCreateAPIView):
queryset = Category.objects.all()
serializer_class = CategorySerializer
permission_classes = [UserIsOwnerPermission]
类别型号:
class Category(models.Model):
name = models.CharField(max_length=50)
description = models.TextField(blank=True, null=True)
color_hexcode = models.CharField(max_length=7, blank=True, null=True)
owner_id = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, null=True)
created_at = models.DateTimeField(default=timezone.now)
class Meta:
ordering = ("-created_at",)
def __str__(self):
return self.name
用户模型
class CustomUnitsUserManager(BaseUserManager):
def create_user(self, email, username, password, **extra_fields):
if not email:
raise ValueError(_("The Email must be set"))
if not username:
raise ValueError(_("The Username must be set"))
email = self.normalize_email(email)
user = self.model(email=email, username=username, **extra_fields)
user.set_password(password)
user.save()
return user
def create_superuser(self, email, username, password, **extra_fields):
extra_fields.setdefault("is_staff", True)
extra_fields.setdefault("is_superuser", True)
extra_fields.setdefault("is_active", True)
if extra_fields.get("is_staff") is not True:
raise ValueError(_("Superuser must have is_staff=True."))
if extra_fields.get("is_superuser") is not True:
raise ValueError(_("Superuser must have is_superuser=True."))
return self.create_user(email, username, password, **extra_fields)
class UnitsUser(AbstractBaseUser, PermissionsMixin):
email = models.EmailField(_("email address"), unique=True)
username = models.CharField(max_length=50, unique=True)
first_name = models.CharField(max_length=50, blank=True)
last_name = models.CharField(max_length=50, blank=True)
date_joined = models.DateTimeField(default=timezone.now)
about = models.TextField(_("about"), blank=True)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
objects = CustomUnitsUserManager()
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["username"]
def __str__(self):
return f"id: {self.id}, username: {self.username}, email: {self.email}"
看起来您正在尝试将字符串“1”与整数 1 匹配,所以这就是失败的原因
class UserIsOwnerPermission(permissions.BasePermission):
"""Permission that checks if authenticated user is the owner of the object being requested or created"""
def has_object_permission(self, request, view, obj):
return obj.owner_id == request.user.id # not related, but here you missed the .id
def has_permission(self, request, view):
# Catch POST requests that have an owner_id that does not match the authenticated user
if "owner_id" in request.data:
self.message = "owner_id field does not match authenticated user"
return str(request.user.id) == str(request.data["owner_id"])
return True
如果你的API应该使用整数,只需更改测试
def test_create_category(self):
self.client.force_authenticate(user=self.user)
data = {
"name": "test category",
"description": "test description",
"owner_id": self.user.id,
}