TIL

23.05.11 TIL

best_spear_man 2023. 5. 12. 04:25

오늘은...

팀 프로젝트를 진행하였다. 팀프로젝트에서 회원탈퇴와 내 회원정보 조회, 내 회원 정보 수정(비밀번호 수정)기능의그 테스트 코드를 픽스 하고 였다.

프로필 페이지 조회 및 수정 기능과, 팔로우/팔로워 기능, 팔로우한 사람의 글 모아보기 기능을 구현하였다.

 

 

1. 테스트코드

class UserBaseTestCase(APITestCase):
    """
    회원가입과 로그인이 필요한 기능들을 위한 부모 클래스입니다.
    """

    @classmethod
    def setUpTestData(cls) -> None:
        cls.user = User.objects.create_user(
            username="zxcvbnasdf_",
            email="abcd@naver.com",
            password="asdf1234!!",
        )
        cls.user_data = {"username": "zxcvbnasdf_", "password": "asdf1234!!"}

    def setUp(self) -> None:
        self.access = self.client.post(reverse("token"), self.user_data).data["access"]

먼저 회원가입과 로그인이 필요한 기능들은 테스트하기위해 위와같은 코드를 비슷하게 반복하여 사용하였으므로 이를 부모 클래스에 넣어두고 자식들이 상속받아 사용하기로 했다.

class UserSignOutTestCase(UserBaseTestCase):
    """
    탈퇴기능을 검정하기 위한 케이스
    """

    def test_logined(self):
    ...
        response = self.client.put(
            ...
            HTTP_AUTHORIZATION=f"Bearer {self.access}",
            ...
        )
        self.assertEqual(response.status_code, 200)

    def test_annon(self):
        ...
        response = self.client.put(
            ...
            # HTTP_AUTHORIZATION 없음
        )
        self.assertEqual(response.status_code, 401)

    def test_wrong_password(self):
        ...
        response = self.client.put(
            ...
            data={"password": "asdf!!1234"}, # 틀린비번
        )
        self.assertEqual(response.status_code, 400)

    def test_wrong_token(self):
        ...
        response = self.client.put(
            ...
            # 틀린 토큰
            HTTP_AUTHORIZATION=f"Bearer {self.access[:-3]}123",
        )
        self.assertEqual(response.status_code, 401)

자식클래스의 예시로 위와같이 탈퇴 테스트 코드를 작성하였고, 잘 작동하였다.

 

 

문제 발생: 유저 수정 테스트 코드의 오류

탈퇴 테스트 코드 이후 유저 수정 테스트 코드를 만들어 실행해보니 다음과 같이 테스트과 통과하지 못하였다. 

문제가 발생한 부분은 다음과 같다.

# 아무것도 변경안함
        data = {
            "current_password": "asdf1234!!",
        }
        response = self.client.patch(
            path=url, HTTP_AUTHORIZATION=f"Bearer {self.access}", data=data
        )
        self.assertEqual(response.status_code, 200)

이 부분에서 아무것도 입력하지 않았다면 아무것도 수정되지 않고 상태코드로 200이 나와야하는데

위와같이 401 즉 유효한 인증 자격증명이 없다는 결과가 나온다.

 

확인해보기 위해 데이터를 출력을 시켜보니, User의 is_active가 False로  저장되어있다. 

 

시도 : 탈퇴에 사용된 유저와 다른 유저 만들어보기

 

class UserPatchTestCase(UserBaseTestCase):
    """
    회원가입과 로그인이 필요한 기능들을 위한 부모 클래스입니다.
    """

    @classmethod
    def setUpTestData(cls) -> None:
        cls.user = User.objects.create_user(
            username="zxcvbnasdf_@",
            email="abcd1@naver.com",
            password="asdf1234!!",
        )
        cls.user_data = {"username": "zxcvbnasdf_@", "password": "asdf1234!!"}

    def setUp(self) -> None:
        self.access = self.client.post(reverse("token"), self.user_data).data["access"]

탈퇴 테스트케이스와 부모 클래스를 공유하고 그 부모 클래스에서 유저 데이터가 생성되고 저장되므로, 혹여 탈퇴로 인해 불활성화 되었나 싶어 위와같이 오버라이딩을 하여 새로운 유저를 만들어보았으나 동일한 오류가 발생하였다.

 

해결: partial=True

def patch(self, request):
        """
        회원정보 변경을 위해 current_passoword를 입력받아 현재 로그인 중인 유저(request.user)와 비교 후
        일치하면 해당 유저의 정보(password...)를 수정한다.
        """
        user = get_object_or_404(User, id=request.user.id)
        serialized = UserEditSerializer(user, request.data, partial=True)
        if serialized.is_valid():
        	...

시리얼라이저를 호출할 때 partial 옵션을 True로 지정하면 수정을 진행할 때 data로 주어지지 않은 필드는 기존 값 그대로 유지된다.

그러나 partial이 기본값(False)라면, is_active같은 자동지정되는 필드도 억지로 변경하려고 하게되고 입력값으로 주어지지 않았으니(None) False가 되는 것이다.

따라서 위와같이 partial 옵션을 주자 해결 되었다.

 

 

2. 팔로우 팔로워 기능

class User(AbstractBaseUser):
    ...
    followings = models.ManyToManyField(
        "self",
        symmetrical=False,
        related_name="followers",
        blank=True,
    )
    ...

팔로우 기능을 위해 위와같이 모델에 manytomanyfield를 추가하였다. related_name 지정을 통해 역참조를 쉽게 할 수 있다.

class FollowView(APIView):
    permission_classes = [permissions.IsAuthenticated]

    def post(self, request, user_id):
        the_user = get_object_or_404(User, id=user_id)
        if the_user == request.user:
            return Response("Can't self follow", status=status.HTTP_400_BAD_REQUEST)
        if the_user in request.user.followings.all():
            request.user.followings.remove(the_user)
            return Response("Unfollow", status=status.HTTP_200_OK)
        else:
            request.user.followings.add(the_user)
            return Response("Follow", status=status.HTTP_200_OK)

위와같이 view를 작성하여 요청이 올때마다 팔로우/팔로우 취소를 반복하게 하였다.

 

 

3. 프로필 페이지 기능

class Profile(models.Model):
    """
    프로필 모델입니다.
    이미지와 bio, 작성시간과 수정시간을 필드로 가진다.
    User와 1대1관계
    """

    class Meta:
        db_table = "profile"

    username = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
    image = models.ImageField(blank=True, upload_to="%Y/%m/")
    bio = models.TextField(blank=True, null=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

User와 1대1관계로 연결된 Profile 모델을 형성하였다. 이 모델은 User객체가 저장될 때 함께 생성되어 저장된다.

class UserSerializer(serializers.ModelSerializer):
    ...
    def create(self, validated_data):
        ...
        user = super().create(validated_data)
        user.set_password(validated_data["password"])  # 비밀번호 저장(해쉬)
        user.save()
        new_profile = Profile()
        new_profile.username = user
        new_profile.save()
        return user

 

 

문제 발생: 프로필페이지에서 유저가 작성한 글 모음 보기

class ProfileSerializer(serializers.ModelSerializer):
    """
    프로필을 조회하기 위한 시리얼라이저
    역참조를 통해 작성한 글들과 이메일을 불러온다.
    팔로우/팔로워 수를 보여준다.
    """

	...
	articles= serializers.SerializerMethodField()
	...
    
	def get_articles(self, obj):
        return obj.username.article_set.all()
        ...

위와같이 유저의 작성글을 역참조를 이용해 불러오려 했으나 다음과 같은 오류가 발생하였다.

RelatedManager object at 0x7ff4e003d1d0> is not JSON serializable

queryset안의 Article 객체들이 시리얼라이저를 거치지 않아 발생하는 문제였다.

시도 1 - serializer 가져오기

from article.serializers import ArticleSerializer
class ProfileSerializer(serializers.ModelSerializer):
    """
    프로필을 조회하기 위한 시리얼라이저
    역참조를 통해 작성한 글들과 이메일을 불러온다.
    팔로우/팔로워 수를 보여준다.
    """

	...
	articles= serializers.SerializerMethodField()
	...
    
	def get_articles(self, obj):
        return ArticleSerializer(obj.username.article_set.all(),many=True)
        ...

위와같이 수정하여 해결할 수 있었으나, 같은 serializers 모듈에서 서로를 참조하면 추후 순환참조등의 오류발생의 원인이 될 수 있지 않겠느냐는 의견이 나왔다. 

해결: view에서 해결

따라서 최종적으로는 다음과 같이 view에서 따로 데이터를 담아 보내는 것으로 수정하였다.

profile = get_object_or_404(Profile, username=user_id)
        if not profile.username.is_active:
            return Response({"message": "탈퇴한 사용자입니다"}, status=status.HTTP_404_NOT_FOUND)
        else:
            serialized = ProfileSerializer(profile)
        response_data = {
            "profile_data": serialized.data,
            "articles": ArticleSerializer(
                profile.username.article_set.all(), many=True
            ).data,
        }