Why would a django-rest-framework test return a different response than an identical post via postman?

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?

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.

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

After walking through the logic, I discovered that there was an error in checking permissions. I was doing it too early and failed to include a parameter. I can't explain why it was working on postman but not on my tests, but changing the following corrected the error:

def perform_create(self, serializer):
        user = self.request.user
        org = None
        # check if user has necessary permissions to create events
        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
        perm = CheckPermission(capability='event', org=org, action='create').has_permission(self.request, self)

I moved the perm = CheckPermission() from above the 'if organizationChange' logic to below it, and that resolved it. The uuid wasn't coming through because the object was not successfully created.

Back to Top