Context:
I'm currently rewriting my django-rest-framework implementation to use a custom permissions class. In the process, I am writing tests to make sure that any future changes don't break anything.
Related Objects:
View
class EventViewSet(viewsets.ModelViewSet):
serializer_class = EventSerializer
permission_classes = [ReadOnly, CheckPermission]
def get_permissions(self):
# This passes the kwargs needed for the permission check to the permission class
if self.action in ['create'] and self.request.user.is_authenticated:
return [CheckPermission(capability='events', action='write')]
if self.action in ['retrieve', 'list']:
return [CheckPermission(capability='events', action='read')]
elif self.action in ['update', 'partial_update'] and self.request.user.is_authenticated:
return [CheckPermission(capability='events', action='edit', org=Organization.objects.get(short_name=self.request.data['organizationChange']))]
elif self.action == 'destroy' and self.request.user.is_authenticated:
return [CheckPermission(capability='events', action='edit')]
return super().get_permissions()
def get_queryset(self):
user = self.request.user
if 'org' in self.request.query_params:
org = Organization.objects.get(short_name=self.request.query_params['org'])
# check if user has necessary permissions for the specific organization
perm = CheckPermission(capability='event', org=org, action='read').has_permission(self.request, self)
if perm:
return Event.objects.filter(organization=org, removed=False)
else:
#return public events for that org
return Event.objects.filter(organization=org, type=0, removed=False)
else:
try:
q1 = Event.objects.filter(type=0, removed=False)
if user.is_authenticated:
memberships = AccountRoleMembership.objects.filter(user=user, removed=False, role__removed=False, role__capabilities__removed=False, role__capabilities__capability__name='event', role__capabilities__read=True)
orgs = get_orgs(memberships)
if orgs.count() > 0:
for org in orgs:
# check if user has necessary permissions for events with a specific organization
perm = CheckPermission(capability='event', org=org, action='read').has_permission(self.request, self)
if perm:
q1 = q1 | Event.objects.filter(organization=org, type=1, removed=False)
return q1
else:
return Event.objects.filter(type=0, removed=False)
except Exception:
return Event.objects.filter(type=0, removed=False)
def get_interested(self, event):
user = self.request.user
return user.is_authenticated and check_permission(user, event.organization, 'event', 'write')
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
my_interest = self.get_my_interest(instance)
interested = self.get_interested(instance)
serializer = self.get_serializer(instance, context={'request': request, 'my_interest': my_interest, 'interested': interested})
return Response(serializer.data)
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
my_interests = {event.id: self.get_my_interest(event) for event in queryset}
interested = {event.id: self.get_interested(event) for event in queryset}
serializer = self.get_serializer(queryset, many=True, context={'request': request, 'my_interests': my_interests, 'interested': interested})
return Response(serializer.data)
def perform_create(self, serializer):
user = self.request.user
org = None
# check if user has necessary permissions to create events
perm = CheckPermission(capability='event', action='create').has_permission(self.request, self)
if 'organizationChange' in self.request.data:
try:
org = Organization.objects.get(short_name=self.request.data['organizationChange'])
except:
print('Organization does not exist')
pass
elif 'guild' in self.request.data:
try:
org = DiscordServer.objects.get(server_id=self.context['request'].data['guild']).org
except:
pass
if perm:
try:
newEvent = serializer.save(owner=user,organization=org, source='scorg')
discordEvent = create_event(newEvent)
if discordEvent is not None:
newEvent.discord_event_id = str(discordEvent)
newEvent.save()
except Event.DoesNotExist:
pass
else:
# return a more informative response to the client if the user does not have the necessary permissions
return Response({"error": perm}, status=403)
...
Serializer
class EventSerializer(serializers.ModelSerializer):
owner = serializers.PrimaryKeyRelatedField(read_only=True)
organization = OrganizationStubSerializer(many=False, allow_null=True, required=False)
short_name = serializers.ReadOnlyField(source='organization.short_name', read_only=True)
canRead = serializers.SerializerMethodField()
canWrite = serializers.SerializerMethodField()
canEdit = serializers.SerializerMethodField()
canDelete = serializers.SerializerMethodField()
interested = serializers.SerializerMethodField()
my_interest = serializers.SerializerMethodField()
jobs = serializers.SerializerMethodField()
partial=True
class Meta:
model = Event
fields = ['uuid','name','source','discord_event_id','my_interest','organization','short_name','content','owner','type','start','end','location','discord_link','discord_event_id','discord_description','discord_location','status','created','interested','jobs','canRead','canWrite','canEdit','canDelete']
def get_canRead(self,obj):
try:
if obj.owner == self.context['request'].user:
return True
except:
pass
if 'organization' in obj:
organization = obj.organization
else:
organization = None
return check_permission(self.context['request'].user, organization, 'event', 'read')
def get_canWrite(self, obj):
try:
if obj.owner == self.context['request'].user:
return True
except:
pass
if 'organization' in obj:
organization = obj.organization
else:
organization = None
return check_permission(self.context['request'].user, organization, 'event', 'write')
def get_canEdit(self, obj):
try:
if obj.owner == self.context['request'].user:
return True
except:
pass
if 'organization' in obj:
organization = obj.organization
else:
organization = None
return check_permission(self.context['request'].user, organization, 'event', 'edit')
def get_canDelete(self,obj):
try:
if obj.owner == self.context['request'].user:
return True
except:
pass
if 'organization' in obj:
organization = obj.organization
else:
organization = None
return check_permission(self.context['request'].user, organization, 'event', 'delete')
def get_jobs(self, obj):
try:
jobs = EventJob.objects.filter(event=obj, parent=None)
return EventJobSerializer(jobs, many=True, context={'request': self.context['request']}).data
except:
return None
def get_interested(self, obj, interested_list=None):
return UserStubSerializer(obj.interested, many=True).data if interested_list else None
def get_my_interest(self, obj):
try:
if self.context['request'].user.is_authenticated:
if self.context['request'].user in obj.interested.all():
return True
else:
return False
else:
return False
except:
return False
def create(self, validated_data):
validated_data['uuid'] = uuid.uuid4()
validated_data['owner'] = self.context['request'].user
event = Event.objects.create(**validated_data)
if event.uuid:
return event
else:
return None
Symptoms:
I run the following test:
def test_event_edit_authenticated_non_org_user(self):
'''test that an event can be edited by an non-org user'''
#self.client.force_authenticate(user=self.user1)
response = self.client.post(
'/backend/api/event/',
{
'name': 'Test Event',
'content': 'This is a test event',
'start': '2020-01-01T00:00:00Z',
'end': '2020-01-01T01:00:00Z',
'organizationChange': self.org1.short_name,
'location': 'Test Location',
},
HTTP_AUTHORIZATION='Token ' self.token1.key,
format='json'
)
self.assertEqual(response.status_code, 201)
event = Event.objects.get(uuid=response.data['uuid'])
#self.client.force_authenticate(user=self.user2)
response = self.client.patch(
'/backend/api/event/' str(event.uuid) '/',
{'uuid': str(event.uuid),
'name': 'Test Event 2',
'content': 'This is a test event',
'start': '2020-01-01T00:00:00Z',
'end': '2020-01-01T01:00:00Z',
'location': 'Test Location',
},
HTTP_AUTHORIZATION='Token ' self.token2.key,
format='json'
)
self.assertEqual(response.status_code, 403)
I receive this error:
======================================================================
ERROR: test_event_edit_authenticated_non_org_user (events.tests.EventTest.test_event_edit_authenticated_non_org_user)
test that an event can be edited by an non-org user
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\env\scorg\django\events\tests.py", line 232, in test_event_edit_authenticated_non_org_user
event = Event.objects.get(uuid=response.data['uuid'])
~~~~~~~~~~~~~^^^^^^^^
KeyError: 'uuid'
I have a similar error in all of my tests which require a uuid as part of the response data.
What I've Tried
1. I verified that the 'uuid' field was not being returned by printing the response.
Response for event creation: {'name': 'Test Event', 'source': None, 'discord_event_id': None, 'my_interest': False, 'organization': None, 'content': 'This is a test event', 'start': '2020-01-01T00:00:00Z', 'end': '2020-01-01T01:00:00Z', 'location': 'Test Location', 'discord_link': None, 'discord_description': None, 'discord_location': None, 'interested': None, 'jobs': None, 'canRead': True, 'canWrite': False, 'canEdit': False, 'canDelete': False}
This validated that the uuid field is not being returned.
2. I verified that 'uuid' is included in the related serializer:
lass EventSerializer(serializers.ModelSerializer):
...
class Meta:
model = Event
fields = ['uuid','name','source','discord_event_id','my_interest','organization','short_name','content','owner','type','start','end','location','discord_link','discord_event_id','discord_description','discord_location','status','created','interested','jobs','canRead','canWrite','canEdit','canDelete']
...
This validated that the 'uuid' field is included in the related serializer.
3. I created a manual post using Postman to test whether a manual post has the same issue.
POST Request:
{
"name": "Test Event2",
"content": "This is a test event",
"start": "2020-01-01T00:00:00Z",
"end": "2020-01-01T01:00:00Z",
"organizationChange": "ESHORES",
"location": "Test Location"
}
Response:
{
"uuid": "7f7568b0-a07c-441d-915d-9ab3d3619a01",
"name": "Test Event2",
"source": "scorg",
"discord_event_id": null,
"my_interest": false,
"organization": {
"uuid": "6fcec5b4-b2b4-49ea-bcb4-93ff585670e4",
"name": "ESHORES",
"short_name": "ESHORES",
"url": "https://robertsspaceindustries.com/orgs/ESHORES/"
},
"short_name": "ESHORES",
"content": "This is a test event",
"owner": 231574499013820417,
"type": 0,
"start": "2020-01-01T00:00:00Z",
"end": "2020-01-01T01:00:00Z",
"location": "Test Location",
"discord_link": null,
"discord_description": null,
"discord_location": null,
"status": 0,
"created": "2023-01-11T20:07:02.783395Z",
"interested": null,
"jobs": [],
"canRead": true,
"canWrite": true,
"canEdit": true,
"canDelete": true
}
Clearly it correctly returns a uuid.
4. I've searched through google/stack/reddit and even asked chatGPT to no avail.
I expected that the test client post and the manual postman post would return the same fields, however, the test client does not. Does anyone have any idea why?
CodePudding user response:
Since you manually insert the 'uuid'
into the validated_data
, it won't reside in response.data
. If you want to assert the actual returned output, you need to check the raw response.content
:
data = json.loads(response.content)
uuid = data['uuid']
This is the actual response body that was returned from the endpoint.
CodePudding user response:
def test_event_edit_authenticated_non_org_user(self):
'''test that an event can be edited by an non-org user'''
#self.client.force_authenticate(user=self.user1)
response = self.client.post(
'/backend/api/event/',
{
'uuid': '79576b8d-2225-42a7-be58-9b61033cbea7',
'name': 'Test Event',
'content': 'This is a test event',
'start': '2020-01-01T00:00:00Z',
'end': '2020-01-01T01:00:00Z',
'organizationChange': self.org1.short_name,
'location': 'Test Location',
},
HTTP_AUTHORIZATION='Token ' self.token1.key,
format='json'
)
self.assertEqual(response.status_code, 201)
event = Event.objects.last()
YOu have to provide UUID manually Now you will use the event variable to get all fields of Event Model