인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

pplkjh2님의 프로필 이미지
pplkjh2

작성한 질문수

파이썬/장고 웹서비스 개발 완벽 가이드 with 리액트 (장고 4.2 기준)

13-16) UpdateAPI test 에러

해결된 질문

작성

·

20

·

수정됨

0

 

강의 수강 내용에 따라

test_author_can_update_post 와 test_non_author_cannot_update_post를 수행하면 하기의 에러가 발생합니다.

list, retrieve, create, destroy 전부 정상 작동하는데 update만 해당 오류가 발생합니다

/blog/api.py, line 75, in has_permission
  if request.method in SAFE_METHODS:
NameError: name 'SAFE_METHODS' is not defined 

 

수업 내용대로 따라가고 있는데 ㅠㅠ 오류가 발생한 곳이 어디인지 알 수 가 없습니다.

수업 내용을 따라서 구현한 코드는 다음과 같습니다.

# blog/tests/test_api.py

import base64

import pytest
from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
from rest_framework import status
from rest_framework.response import Response
from rest_framework.test import APIClient

from accounts.models import User
from accounts.tests.factories import UserFactory
from blog.models import Post
from blog.tests.factories import PostFactory


def create_user(raw_password: str = None) -> User:
    """새로운 User 레코드를 생성 및 반환"""
    return UserFactory(raw_password=raw_password)


def get_api_client_with_basic_auth(user: User, raw_password: str) -> APIClient:
    """인자의 User 인스턴스와 암호 기반에서 Basic 인증을 적용한 APIClient 인스턴스 반환"""

    # *.http 파일에서는 자동으로 base64 인코딩을 수행해줬었습니다.
    base64_data: bytes = f"{user.username}:{raw_password}".encode()
    authorization_header: str = base64.b64encode(base64_data).decode()

    client = APIClient()
    client.credentials(HTTP_AUTHORIZATION=f"Basic {authorization_header}")
    return client


@pytest.fixture
def unauthenticated_api_client() -> APIClient:
    """Authorization 인증 헤더가 없는 기본 APIClient 인스턴스 반환"""
    return APIClient()


@pytest.fixture
def api_client_with_new_user_basic_auth(faker) -> APIClient:
    """새로운 User 레코드를 생성하고, 그 User의 인증 정보가 Authorization 헤더로 지정된 APIClient 인스턴스 반환"""
    raw_password: str = faker.password()
    user: User = create_user(raw_password)
    api_client: APIClient = get_api_client_with_basic_auth(user, raw_password)
    return api_client


@pytest.fixture
def new_user() -> User:
    """새로운 User 레코드를 생성 및 반환"""
    return create_user()


@pytest.fixture
def new_post() -> Post:
    """새로운 Post 레코드를 반환"""
    return PostFactory()


@pytest.mark.it("작성자가 아닌 유저가 수정 요청하면 거부")
@pytest.mark.django_db
def test_non_author_cannot_update_post(new_post, api_client_with_new_user_basic_auth):
    url = reverse("api-v1:post_edit", args=[new_post.pk])
    response: Response = api_client_with_new_user_basic_auth.patch(url, data={})
    assert status.HTTP_403_FORBIDDEN == response.status_code


@pytest.mark.it("작성자가 수정 요청하면 성공")
@pytest.mark.django_db
def test_author_can_update_post(faker):
    raw_password = faker.password()
    author = create_user(raw_password=raw_password)
    created_post = PostFactory(author=author)

    url = reverse("api-v1:post_edit", args=[created_post.pk])
    api_client = get_api_client_with_basic_auth(author, raw_password)
    data = {"title": faker.sentence()}
    response: Response = api_client.patch(url, data=data)
    assert status.HTTP_200_OK == response.status_code
    assert data["title"] == response.data["title"]
## core/mixins.py
from typing import List, Optional, Type

from colorama import Fore
from django.conf import settings
from django.db.models import Model, QuerySet
from rest_framework import permissions
from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.utils.serializer_helpers import ReturnDict
from rest_framework.views import APIView

from core.permissions import make_drf_permission_class


class JSONResponseWrapperMixin:
    def finalize_response(
        self, request: Request, response: Response, *args, **kwargs
    ) -> Response:
        is_ok = 200 <= response.status_code < 400
        accepted_renderer = getattr(request, "accepted_renderer", None)

        if accepted_renderer is None or response.exception is True:
            response.data = {
                "ok": is_ok,
                "result": response.data,
            }
        elif isinstance(
            request.accepted_renderer, (JSONRenderer, BrowsableAPIRenderer)
        ):
            response.data = ReturnDict(
                {
                    "ok": is_ok,
                    "result": response.data,  # ReturnList
                },
                serializer=response.data.serializer,
            )

        return super().finalize_response(request, response, *args, **kwargs)


class PermissionDebugMixin:
    if settings.DEBUG:

        def get_label_text(self, is_permit: bool) -> str:
            return (
                f"{Fore.GREEN}Permit{Fore.RESET}"  # colorama 라이브러리 활용
                if is_permit
                else f"{Fore.RED}Deny{Fore.RESET}"
            )

        def check_permissions(self, request: Request) -> None:
            print(f"{request.method} {request.path} has_permission")
            for permission in self.get_permissions():
                is_permit: bool = permission.has_permission(request, self)
                print(
                    f"\t{permission.__class__.__name__} = {self.get_label_text(is_permit)}"
                )
                if not is_permit:
                    self.permission_denied(
                        request,
                        message=getattr(permission, "message", None),
                        code=getattr(permission, "code", None),
                    )

        def check_object_permissions(self, request: Request, obj: Model) -> None:
            print(f"{request.method} {request.path} has_object_permission")
            for permission in self.get_permissions():
                is_permit: bool = permission.has_object_permission(request, self, obj)
                print(
                    f"\t{permission.__class__.__name__} = {self.get_label_text(is_permit)}"
                )
                if not is_permit:
                    self.permission_denied(
                        request,
                        message=getattr(permission, "message", None),
                        code=getattr(permission, "code", None),
                    )


class TestFuncPermissionMixin:
    TEST_FUNC_PERMISSION_CLASS_NAME = "TestFuncPermissionMixin"

    @classmethod
    def get_test_func_permission_instance(cls) -> permissions.BasePermission:
        permission_class = make_drf_permission_class(
            class_name=cls.TEST_FUNC_PERMISSION_CLASS_NAME,
            # *_test_func_name 속성이 지정되면, 이 권한 클래스가 사용된 APIView 클래스에서
            # 지정 이름의 메서드를 찾습니다.
            has_permission_test_func_name="has_permission",
            has_object_permission_test_func_name="has_object_permission",
        )
        return permission_class()

    def get_permissions(self) -> List[permissions.BasePermission]:
        # 기존 permission_classes 설정에 권한 정책을 추가하는 방식으로 동작
        return super().get_permissions() + [self.get_test_func_permission_instance()]

    def has_permission(self, request: Request, view: APIView) -> bool:
        return True

    def has_object_permission(
        self, request: Request, view: APIView, obj: Model
    ) -> bool:
        return True

 

답변 2

1

이진석님의 프로필 이미지
이진석
지식공유자

안녕하세요. SAFE_METHODS 값은 rest_framework.permissions 에 SAFE_METHODS 가 정의되어있습니다. 보여주신 코드에서도 permissions.SAFE_METHODS 코드로 참조하신 부분이 있네요.

실제 예외가 발생한 곳은 /blog/api.py 인데요.

image.png

blog/api.py 코드에서 임포트 코드가 누락되신 듯 합니다. 아래 코드를 추가해보시겠어요?

from rest_framework.permissions import SAFE_METHODS

살펴보시고 댓글 부탁드립니다.

화이팅입니다. :-)

pplkjh2님의 프로필 이미지
pplkjh2
질문자

감사합니다 해결되었습니다. 이걸 빼먹었네요 ㅠㅠ 코드 부분만 확인하다가 헤맸습니다. vscode 로 작업 중인데 pylance가 안먹는 매서드들이 존재해 debug할 때 어렵네요

0

pplkjh2님의 프로필 이미지
pplkjh2
질문자

10000자 제한으로 이어 답글 답니다

# core/permissions.py
from typing import Callable, Type, cast

from django.db.models import Model
from rest_framework import permissions
from rest_framework.request import Request
from rest_framework.views import APIView


class IsAuthorOrReadonly(permissions.BasePermission):
    def has_permission(self, request: Request, view: APIView) -> bool:
        if request.method in permissions.SAFE_METHODS:
            return True
        return request.user.is_authenticated

    # 2차 필터
    def has_object_permission(
        self, request: Request, view: APIView, obj: Model
    ) -> bool:
        if request.method in permissions.SAFE_METHODS:
            return True

        if not hasattr(obj, "author"):
            return False

        return obj.author == request.user


def make_drf_permission_class(
    class_name: str = "PermissionClass",
    permit_safe_methods: bool = False,
    has_permission_test_func: Callable[[Request, APIView], bool] = None,
    has_permission_test_func_name: str = None,
    has_object_permission_test_func: Callable[[Request, APIView, Model], bool] = None,
    has_object_permission_test_func_name: str = None,
) -> Type[permissions.BasePermission]:

    def has_permission(self, request: Request, view: APIView) -> bool:
        if permit_safe_methods and request.method in permissions.SAFE_METHODS:
            return True
        if has_permission_test_func is not None:
            return has_permission_test_func(request, view)
        if has_permission_test_func_name is not None:
            test_func = getattr(view, has_permission_test_func_name)
            return test_func(request, view)
        return True

    def has_object_permission(
        self, request: Request, view: APIView, obj: Model
    ) -> bool:
        if permit_safe_methods and request.method in permissions.SAFE_METHODS:
            return True
        if has_object_permission_test_func is not None:
            return has_object_permission_test_func(request, view, obj)
        if has_object_permission_test_func_name is not None:
            test_func = getattr(view, has_object_permission_test_func_name)
            return test_func(request, view, obj)
        return True

    permission_class = type(
        class_name,
        (permissions.BasePermission,),
        {
            "has_permission": has_permission,
            "has_object_permission": has_object_permission,
        },
    )

    return cast(Type[permissions.BasePermission], permission_class)


class TestFuncPermissionMixin:
    TEST_FUNC_PERMISSION_CLASS_NAME = None

    @classmethod
    def get_test_func_permission_instance(cls) -> permissions.BasePermission:
        permission_class = make_drf_permission_class(
            class_name=cls.TEST_FUNC_PERMISSION_CLASS_NAME or cls.__name__,
            has_permission_test_func_name="has_permission",
            has_object_permission_test_func_name="has_object_permission",
        )
        return permission_class()

    def get_permissions(self) -> list[permissions.BasePermission]:
        return super().get_permissions() + [self.get_test_func_permission_instance()]

    def has_permission(self, request: Request, view: APIView) -> bool:
        return True

    def has_object_permission(
        self, request: Request, view: APIView, obj: Model
    ) -> bool:
        return True


PermitSafeMethods = make_drf_permission_class(
    permit_safe_methods=True,
)
pplkjh2님의 프로필 이미지
pplkjh2

작성한 질문수

질문하기