Add custom field validators and permission classes with tests

This commit is contained in:
Ghosts6 2024-10-19 18:25:23 +01:00
parent d3dd45b3f4
commit b47bff0db5
4 changed files with 196 additions and 1 deletions

View File

@ -882,6 +882,38 @@ class IPAddressField(CharField):
return super().to_internal_value(data)
class AlphabeticFieldValidator:
"""
Custom validator to ensure that a field only contains alphabetic characters and spaces.
"""
def __call__(self, value):
if not re.match(r'^[A-Za-z ]*$', value):
raise ValueError("This field must contain only alphabetic characters and spaces.")
class AlphanumericFieldValidator:
"""
Custom validator to ensure the field contains only alphanumeric characters (letters and numbers).
"""
def __call__(self, value):
if not re.match(r'^[A-Za-z0-9]*$', value):
raise ValueError("This field must contain only alphanumeric characters (letters and numbers).")
class CustomLengthValidator:
"""
Custom validator to ensure the length of a string is within specified limits.
"""
def __init__(self, min_length=0, max_length=None):
self.min_length = min_length
self.max_length = max_length
def __call__(self, value):
if len(value) < self.min_length:
raise ValueError(f"This field must be at least {self.min_length} characters long.")
if self.max_length is not None and len(value) > self.max_length:
raise ValueError(f"This field must be no more than {self.max_length} characters long.")
# Number types...
class IntegerField(Field):

View File

@ -173,6 +173,32 @@ class IsAuthenticatedOrReadOnly(BasePermission):
)
class IsAdminUserOrReadOnly(BasePermission):
"""
Custom permission to only allow admin users to edit an object.
"""
def has_permission(self, request, view):
# Allow any user to view the object
if request.method in ['GET', 'HEAD', 'OPTIONS']:
return True
# Only allow admin users to modify the object
return request.user and request.user.is_staff
class IsOwner(BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
# Allow read-only access to any request
if request.method in ['GET', 'HEAD', 'OPTIONS']:
return True
# Write permissions are only allowed to the owner of the object
return obj.owner == request.user
class DjangoModelPermissions(BasePermission):
"""
The request is authenticated using `django.contrib.auth` permissions.

View File

@ -25,7 +25,7 @@ from django.utils.timezone import activate, deactivate, override
import rest_framework
from rest_framework import exceptions, serializers
from rest_framework.fields import (
BuiltinSignatureError, DjangoImageField, SkipField, empty,
AlphabeticFieldValidator, AlphanumericFieldValidator, BuiltinSignatureError, CustomLengthValidator, DjangoImageField, SkipField, empty,
is_simple_callable
)
from tests.models import UUIDForeignKeyTarget
@ -1061,6 +1061,88 @@ class TestFilePathField(FieldValues):
)
class TestAlphabeticField:
"""
Valid and invalid values for `AlphabeticFieldValidator`.
"""
valid_inputs = {
'John Doe': 'John Doe',
'Alice': 'Alice',
'Bob Marley': 'Bob Marley',
}
invalid_inputs = {
'John123': ['This field must contain only alphabetic characters and spaces.'],
'Alice!': ['This field must contain only alphabetic characters and spaces.'],
'': ['This field must contain only alphabetic characters and spaces.'],
}
field = str # Placeholder for the field type
def test_valid_inputs(self):
validator = AlphabeticFieldValidator()
for value in self.valid_inputs.keys():
validator(value) # Should not raise ValueError
def test_invalid_inputs(self):
validator = AlphabeticFieldValidator()
for value, expected_errors in self.invalid_inputs.items():
with pytest.raises(ValueError) as excinfo:
validator(value)
assert str(excinfo.value) == expected_errors[0]
class TestAlphanumericField:
"""
Valid and invalid values for `AlphanumericFieldValidator`.
"""
valid_inputs = {
'John123': 'John123',
'Alice007': 'Alice007',
'Bob1990': 'Bob1990',
}
invalid_inputs = {
'John!': ['This field must contain only alphanumeric characters (letters and numbers).'],
'Alice 007': ['This field must contain only alphanumeric characters (letters and numbers).'],
'': ['This field must contain only alphanumeric characters (letters and numbers).'],
}
field = str # Placeholder for the field type
def test_valid_inputs(self):
validator = AlphanumericFieldValidator()
for value in self.valid_inputs.keys():
validator(value) # Should not raise ValueError
def test_invalid_inputs(self):
validator = AlphanumericFieldValidator()
for value, expected_errors in self.invalid_inputs.items():
with pytest.raises(ValueError) as excinfo:
validator(value)
assert str(excinfo.value) == expected_errors[0]
class TestCustomLengthField:
"""
Valid and invalid values for `CustomLengthValidator`.
"""
valid_inputs = {
'abc': 'abc', # 3 characters
'abcdefghij': 'abcdefghij', # 10 characters
}
invalid_inputs = {
'ab': ['This field must be at least 3 characters long.'], # Too short
'abcdefghijk': ['This field must be no more than 10 characters long.'], # Too long
}
field = str # Placeholder for the field type
def test_valid_inputs(self):
validator = CustomLengthValidator(min_length=3, max_length=10)
for value in self.valid_inputs.keys():
validator(value) # Should not raise ValueError
def test_invalid_inputs(self):
validator = CustomLengthValidator(min_length=3, max_length=10)
for value, expected_errors in self.invalid_inputs.items():
with pytest.raises(ValueError) as excinfo:
validator(value)
assert str(excinfo.value) == expected_errors[0]
# Number types...
class TestIntegerField(FieldValues):

View File

@ -772,3 +772,58 @@ class PermissionsCompositionTests(TestCase):
]
assert filtered_permissions == expected_permissions
class PermissionTests(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.admin_user = User.objects.create_user(username='admin', password='password', is_staff=True)
self.regular_user = User.objects.create_user(username='user', password='password')
self.anonymous_user = AnonymousUser()
def test_is_admin_user_or_read_only_allow_read(self):
# Test that all users can read
request = self.factory.get('/1', format='json')
request.user = self.anonymous_user
permission = permissions.IsAdminUserOrReadOnly()
self.assertTrue(permission.has_permission(request, None)) # Allow read access
request.user = self.admin_user # Admin user
self.assertTrue(permission.has_permission(request, None)) # Allow read access for admin
def test_is_admin_user_or_read_only_allow_write(self):
# Test that only admin users can write
request = self.factory.post('/1', format='json')
request.user = self.admin_user # Admin user
permission = permissions.IsAdminUserOrReadOnly()
self.assertTrue(permission.has_permission(request, None)) # Allow write access for admin
request.user = self.regular_user # Non-admin user
self.assertFalse(permission.has_permission(request, None)) # Deny write access for non-admin
def test_is_owner_permission(self):
# Setup mock object with owner
obj = BasicModel(owner=self.admin_user) # Assuming BasicModel has an owner field
# Test that the owner can modify
request = self.factory.post('/1', format='json')
request.user = self.admin_user
permission = permissions.IsOwner()
self.assertTrue(permission.has_object_permission(request, None, obj)) # Owner should have access
# Test that a non-owner cannot modify
request.user = self.regular_user # Another user
self.assertFalse(permission.has_object_permission(request, None, obj)) # Another user should not have access
def test_is_owner_read_access(self):
# Setup mock object with owner
obj = BasicModel(owner=self.admin_user) # Assuming BasicModel has an owner field
# Test read access for any user
request = self.factory.get('/1', format='json')
request.user = self.regular_user # Another user
permission = permissions.IsOwner()
self.assertTrue(permission.has_object_permission(request, None, obj)) # Any user can read
request.user = self.admin_user # The owner
self.assertTrue(permission.has_object_permission(request, None, obj)) # Owner can read