오늘은...
팀 프로젝트를 진행하였다. 팀프로젝트에서 회원탈퇴와 내 회원정보 조회, 내 회원 정보 수정(비밀번호 수정)기능의그 테스트 코드를 픽스 하고 였다.
프로필 페이지 조회 및 수정 기능과, 팔로우/팔로워 기능, 팔로우한 사람의 글 모아보기 기능을 구현하였다.
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,
}
'TIL' 카테고리의 다른 글
23.05.15 TIL (0) | 2023.05.17 |
---|---|
23.05.12~14 TIL (0) | 2023.05.17 |
23.05.10 TIL (0) | 2023.05.11 |
23.05.09 TIL (0) | 2023.05.10 |
23.05.02 TIL (0) | 2023.05.03 |