Search code examples
pythondjangodjango-rest-frameworkdjango-testing

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?


Solution

  • 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.