Dev./Django & DRF

[Cloud] Django: Code develop for TEST2

Ivan'show 2023. 8. 31.
728x90
반응형

Github Actions 에서 work flow 를 분리했다. 이제 django 쪽에서 test 를 위한 일부 코드를 작성해 보자.

 

TEST 를 위한 기능 디벨롭, 그리고 테스트 코드 실행

현재 제공하고 있는 기능은 Topic 을 생성하고 그 아래에 Post 들이 One to Many 의 형태로 묶여있는 구조이다. 또 부여된 그룹에 따라 권한이 달라져 접근하거나 작성하는데 제한을 두고 있다.

이 부분들을 테스트 하기 위해서는 유저를 생성하면서 권한을 부여하고, Topic 과 Post 를 생성하는 과정에서 권한을 체크하는 로직이 필요할 것이다.

이를 테스트 하기 위해서 로직을 구현해보자

장고의 로직 자체는 클라이언트가 urls 를 통해 요청을 보내면 views 에서 모델과 templates 을 엮어주는 구조인데, API 를 사용할 예정이니 templates 는 접어두고 models 와 serializer 를 고려하면서 작성한다.

우선 기본기능만 구현되어 있던 views.py 를 업데이트 해보자

from django.shortcuts import get_object_or_404
from django.db.models import Q
from drf_spectacular.utils import extend_schema
from rest_framework import viewsets, status
from rest_framework.request import Request
from rest_framework.response import Response

from .models import Topic, Post, TopicGroupUser
from .serializers import TopicSerializer, PostSerializer

@extend_schema(tags=["Topic"])
class TopicViewSet(viewsets.ModelViewSet):
    queryset = Topic.objects.all()
    serializer_class = TopicSerializer
    @extend_schema(summary="새 토픽 생성")
    def create(self, request, *args, **kwargs):
        return super().create(request, *args, **kwargs)

@extend_schema(tags=["Post"])
class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

    def create(self, request: Request, *args, **kwargs):
        user = request.user
        data = request.data
        topic_id = data.get("topic")
        topic = get_object_or_404(Topic, id=topic_id)
        if topic.is_private:
            qs = TopicGroupUser.objects.filter(
                group__lte=TopicGroupUser.groupChoices.common,
                topic=topic,
                user=user,
            )
            if not qs.exists():
                return Response(
                    status=status.HTTP_401_UNAUTHORIZED,
                    data="This user is not allowed to write a post on this topic",
                )

        return super().create(request, *args, **kwargs)

@extend_schema(tags=["Post"])

Post 모델과 관련된 작업을 다루고 “Post” 태그로 스키마를 확장하려고 한다.

queryset = Post.objects.all()

초기 쿼리셋을 모든 Post 모델의 인스턴스로 설정한다. 게시물을 검색하거나 업데이트하고 삭제하는 등의 작업을 할 때 사용된다.

serializer_class = PostSerializer

시리얼라이즈 클래스를 지정한다. Post 모델의 데이터를 직렬화하거나 역직렬화하는데 사용된다. (feat. JSON)

def create(self, request: Request, *args, **kwargs):

새 게시물을 생성하는 데 사용되는 create 메서드를 정의한다. self, request 를 인수로 받으며 추가로 args, kwargs 를 받는다.

user = request.user

요청을 보낸 사용자의 정보를 가져온다. 인증에 사용될 예정.

data = request.data

요청에서 데이터를 추출한다.

topic_id = data.get("topic")

요청 데이터에서 topic 필드의 값을 가져오려고 시도한다.

topic = get_object_or_404(Topic, id=topic_id)

topic_id 를 사용하여 Topic 객체를 가져온다. 객채가 없는 경우 404 예외를 발생.

if topic.is_private:
            qs = TopicGroupUser.objects.filter(
                group__lte=TopicGroupUser.groupChoices.common,
                topic=topic,
                user=user,
            )
            if not qs.exists():
                return Response(
                    status=status.HTTP_401_UNAUTHORIZED,
                    data="This user is not allowed to write a post on this topic",
                )

이 부분은 TopicGroupUser 모델을 필터링 하는 부분이다. 가져온 topic 의 is_private 값을 체크하고 만약 private 이라면 쿼리를 만들어 검사하고 결과 값을 반환한다.

인증 인가를 위해서는 PERMISSION_CLASSES 를 변경해야한다.

# before
REST_FRAMEWORK = {
    # Use Django's standard `django.contrib.auth` permissions,
    # or allow read-only access for unauthenticated users.
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly"
    ],
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}

# after
REST_FRAMEWORK = {
    # Use Django's standard `django.contrib.auth` permissions,
    # or allow read-only access for unauthenticated users.
    "DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
    "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
}

기존 설정은 인증된 사용자에게 Django 모델의 권한을 부여하고 인증되지 않은 사용자에게 읽기 전용권한을 부여한다.

바뀐 설정은 인증된 사용자에게만 접근 권한을 부여하며 인증이 되지 않은 사용자는 읽기 권한도 없다.

테스트를 진행하는 테스트 파일을 만들어 보자

# tests.py
import json

from django.contrib.auth.models import User
from django.urls import reverse
from django.http import HttpResponse
from rest_framework.test import APITestCase
from rest_framework import status

from .models import Topic, Post, TopicGroupUser

class PostTest(APITestCase):
    @classmethod
    def setUpTestData(cls):
        cls.superuser = User.objects.create_superuser("supueruser")
        cls.private_topic = Topic.objects.create(
            name="private topic",
            is_private=True,
            owner=cls.superuser,
        )
        cls.authorized_user = User.objects.create_user("authorized")
        cls.unauthorized_user = User.objects.create_user("unauthorized")
        TopicGroupUser.objects.create(
            topic=cls.private_topic,
            group=TopicGroupUser.GroupChoices.common,
            user=cls.authorized_user,
        )

    def test_write_permission_on_private_topic(self):
        data = {
            "title": "test",
            "content": "test",
            "topic": self.private_topic.pk,
        }

        # when unauthorized user tries to write a post on Topic => fail. 401
        self.client.force_login(self.unauthorized_user)
        data["owner"] = self.unauthorized_user.pk
        res = self.client.post(reverse("post-list"), data=data)
        self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED)

        # when authorized user tries to write a post on Topic => success. 201
        self.client.force_login(self.authorized_user)
        data["owner"] = self.authorized_user.pk
        res: HttpResponse = self.client.post(reverse("post-list"), data=data)
        self.assertEqual(res.status_code, status.HTTP_201_CREATED)
        data = json.loads(res.content)
        Post.objects.get(pk=data["id"])

class PostTest(APITestCase):

PostTest 클래스로 HTTP 요청을 보내고 응답도 받는 메서드들을 포함한 APITestCase 를 상속받는다.

@classmethod

def setUpTestData(cls):

데코레이터는 해당 메서드가 클래스 메서드임을 나타낸다. 클래스 메서드는 인스턴스 메서드와 달리 클래스 자체에 작동하며 인스턴스를 통해 호출될 필요가 없다.

클래스 메서드는 첫 번째 인자로 클래스 자체를 받게 되며, 이는 일반적으로 cls 로 표기된다.

이제 setUpTestData 메서드는 @classmethod 데코레이터를 사용함으로써 테스트 케이스 클래스 전체에 걸쳐 한 번만 실행되는 메서드가 된다.

setUpTestData 안에는 테스트를 위한 유저와, 토픽등을 생성하고 이를 권한으로 구분짓는다.

TopicGroupUser 를 이용하여 인증된 사용자를 private 토픽과 연결하게 된다.

def test_write_permission_on_private_topic(self): 테스트용 데이터를 생성하고 테스트용 유저 로그인, 요청 까지 만들어 해당 정보를 바탕으로 리턴을 할 수 있게 구성해 놓는다.

e.g.

self.client.force_login(self.unauthorized_user)

강제로 권한이 없는 유저로 로그인

res = self.client.post(reverse("post-list"), data=data)

reverse 형태로 클라이언트 요청을 데이터에 데이터를 담아 전달

현재 코드를 구현한 곳은 로컬이기 때문에, docker-compose 로 컨테이너를 올리고 그 아래의 명령어로 테스트를 실행한다.

docker-compose up --build -d
docker-compose exec lion-app python3 manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.044s

OK

그럼 순서대로 tests.py 가 단일로 실행되면서 작성해놓은 테스트 코드가 실행된다.

728x90
반응형

댓글