diff --git a/apps/chat/services/message_service.py b/apps/chat/services/message_service.py index e69de29b..951e6976 100644 --- a/apps/chat/services/message_service.py +++ b/apps/chat/services/message_service.py @@ -0,0 +1,51 @@ +from django.core.exceptions import PermissionDenied, ValidationError +from django.db import IntegrityError + +from apps.chat.models import ChatMessage +from apps.study_groups.models import GroupMember, StudyGroup +from apps.users.models import User + + +class MessageService: + # 메시지 생성 및 단건 조회 기능 제공 + + @staticmethod + def validate_member(study_group: StudyGroup, user: User) -> None: + # 해당 사용자가 그룹 구성원이 맞는지 검증 + is_member = GroupMember.objects.filter( + study_group_id=study_group.id, + user_id=user.id, # mypy 오류로 user 대신 user.id를 넘겼습니다. + ).exists() + + if not is_member: + raise PermissionDenied("그룹 멤버만 메시지를 작성할 수 있습니다.") + + @staticmethod + def create_message( + study_group: StudyGroup, + user: User, + content: str, + ) -> ChatMessage: + + if not content or not content.strip(): + raise ValidationError("메시지 내용은 비어 있을 수 없습니다.") + + MessageService.validate_member(study_group, user) + + try: + return ChatMessage.objects.create( + study_group=study_group, + sender=user, + content=content.strip(), + ) + except IntegrityError: + raise ValidationError("메시지를 생성할 수 없습니다.") + + @staticmethod + def get_message(message_id: int) -> ChatMessage: + msg = ChatMessage.objects.select_related("sender").filter(id=message_id).first() + + if msg is None: + raise ValidationError("메시지를 찾을 수 없습니다.") + + return msg diff --git a/apps/chat/tests/test_chatroom_service.py b/apps/chat/tests/test_chatroom_service.py new file mode 100644 index 00000000..57822200 --- /dev/null +++ b/apps/chat/tests/test_chatroom_service.py @@ -0,0 +1,151 @@ +import time + +from django.core.exceptions import PermissionDenied +from django.test import TestCase + +from apps.chat.models.chat_message import ChatMessage +from apps.chat.models.last_read_message import LastReadMessage +from apps.chat.services.chatroom_service import ChatRoomService +from apps.study_groups.models import GroupMember, StudyGroup +from apps.users.models.users import User + + +class TestChatRoomService(TestCase): + + def setUp(self) -> None: + # 사용자 + self.user = User.objects.create( + email="user@example.com", + name="김오즈", + nickname="테스트", + phone_number="01012345678", + gender="M", + birthday="1999-01-01", + ) + + self.other_user = User.objects.create( + email="hayeong@example.com", + name="송하영", + nickname="ha0", + phone_number="01012341234", + gender="F", + birthday="1997-09-29", + ) + + # 스터디 그룹 + self.group = StudyGroup.objects.create( + name="Django Study", + introduction="스터디 소개", + max_headcount=5, + start_at="2025-12-01T00:00:00Z", + end_at="2025-12-30T00:00:00Z", + ) + + # user만 그룹 멤버로 등록 + GroupMember.objects.create( + study_group_id=self.group, + user_id=self.user, + is_leader=True, + ) + + # 테스트용 메시지 생성 + self.msg1 = ChatMessage.objects.create( + study_group=self.group, + sender=self.user, + content="첫 번째 메시지", + ) + time.sleep(0.001) # 생성 시간 차이를 내려고 넣었습니다 + + self.msg2 = ChatMessage.objects.create( + study_group=self.group, + sender=self.user, + content="두 번째 메시지", + ) + + # 멤버 검증 테스트 + def test_validate_member_success(self) -> None: + # 그룹에 속한 사용자는 검증 통과 + ChatRoomService.validate_member(self.group, self.user) + + def test_validate_member_fail(self) -> None: + # 그룹에 속하지 않은 사용자는 PermissionDenied + with self.assertRaises(PermissionDenied): + ChatRoomService.validate_member(self.group, self.other_user) + + def test_get_messages(self) -> None: + # 해당 그룹의 메시지 목록을 시간순으로 반환 + messages = ChatRoomService.get_messages(self.group, self.user) + self.assertEqual(messages.count(), 2) + + def test_get_room_info(self) -> None: + info = ChatRoomService.get_room_info(self.group, self.user) + self.assertEqual(info["group_id"], self.group.id) + self.assertEqual(len(info["members"]), 1) + self.assertEqual(info["members"][0]["nickname"], self.user.nickname) + + def test_mark_all_read(self) -> None: + ChatRoomService.mark_all_read(self.group, self.user) + + record = LastReadMessage.objects.get( + study_group=self.group, + user=self.user, + ) + self.assertEqual(record.message.id, self.msg2.id) + + def test_mark_all_read_no_messages(self) -> None: + # 메시지가 전혀 없는 새로운 그룹 생성 + empty_group = StudyGroup.objects.create( + name="Empty Group", + introduction="메시지 없음", + max_headcount=5, + start_at="2025-12-01T00:00:00Z", + end_at="2025-12-30T00:00:00Z", + ) + + GroupMember.objects.create( + study_group_id=empty_group, + user_id=self.user, + is_leader=True, + ) + + # 실행 + ChatRoomService.mark_all_read(empty_group, self.user) + + # 메시지가 없으면 last_read 레코드도 x + record = LastReadMessage.objects.filter( + study_group=empty_group, + user=self.user, + ).first() + + self.assertIsNone(record) + + def test_get_chatrooms_basic(self) -> None: + # 채팅방 목록 조회 기본 테스트 + result = ChatRoomService.get_chatrooms(self.user) + + self.assertEqual(len(result), 1) + + room = result[0] + + # 그룹 id 검증 + self.assertEqual(room["group_id"], self.group.id) + + # 그룹 이름 검증 + self.assertEqual(room["group_name"], self.group.name) + + # last_read x -> unread count = 전체 메시지 개수 + total_messages = ChatMessage.objects.filter(study_group=self.group).count() + + self.assertEqual(room["unread_count"], total_messages) + + def test_get_chatrooms_with_last_read(self) -> None: + LastReadMessage.objects.create( + study_group=self.group, + user=self.user, + message=self.msg1, + ) + + result = ChatRoomService.get_chatrooms(self.user) + + self.assertEqual(len(result), 1) + self.assertEqual(result[0]["unread_count"], 1) diff --git a/apps/chat/tests/test_last_read_service.py b/apps/chat/tests/test_last_read_service.py new file mode 100644 index 00000000..d778d646 --- /dev/null +++ b/apps/chat/tests/test_last_read_service.py @@ -0,0 +1,105 @@ +from django.core.exceptions import PermissionDenied +from django.test import TestCase + +from apps.chat.models.chat_message import ChatMessage +from apps.chat.models.last_read_message import LastReadMessage +from apps.chat.services.last_read_service import LastReadService +from apps.study_groups.models import GroupMember, StudyGroup +from apps.users.models.users import User + + +class TestLastReadService(TestCase): + + def setUp(self) -> None: + # 사용자 + self.user = User.objects.create( + email="test@example.com", + name="김오즈", + nickname="테스터", + phone_number="01012345678", + gender="M", + birthday="1999-01-01", + ) + + self.other_user = User.objects.create( + email="hayeong@example.com", + name="송하영", + nickname="ha0", + phone_number="01012341234", + gender="F", + birthday="1997-09-29", + ) + + self.group = StudyGroup.objects.create( + name="Django Study", + introduction="스터디 소개", + max_headcount=5, + start_at="2025-12-01T00:00:00Z", + end_at="2025-12-30T00:00:00Z", + ) + + GroupMember.objects.create( + study_group_id=self.group, + user_id=self.user, + is_leader=True, + ) + + self.message1 = ChatMessage.objects.create( + study_group=self.group, + sender=self.user, + content="메시지 1", + ) + + self.message2 = ChatMessage.objects.create( + study_group=self.group, + sender=self.user, + content="메시지 2", + ) + + def test_update_last_read_create(self) -> None: + # 처음 읽을 경우 LastReadMessage 생성 + LastReadService.update_last_read( + study_group=self.group, + user=self.user, + message=self.message1, + ) + + record = LastReadMessage.objects.filter( + study_group=self.group, + user=self.user, + ).first() + + self.assertIsNotNone(record) + assert record is not None + self.assertEqual(record.message.id, self.message1.id) + + def test_update_last_read_update(self) -> None: + # 이미 읽은 과거가 있으면 제일 최신 메시지로 업데이트 + + LastReadMessage.objects.create( + study_group=self.group, + user=self.user, + message=self.message1, + ) + + LastReadService.update_last_read( + study_group=self.group, + user=self.user, + message=self.message2, + ) + + updated = LastReadMessage.objects.get( + study_group=self.group, + user=self.user, + ) + + self.assertEqual(updated.message.id, self.message2.id) + + def test_update_last_read_fail_other_user(self) -> None: + # 그룹 구성원이 아니라면 PermissionDenied + with self.assertRaises(PermissionDenied): + LastReadService.update_last_read( + study_group=self.group, + user=self.other_user, + message=self.message1, + ) diff --git a/apps/chat/tests/test_message_service.py b/apps/chat/tests/test_message_service.py new file mode 100644 index 00000000..5d02c0ea --- /dev/null +++ b/apps/chat/tests/test_message_service.py @@ -0,0 +1,89 @@ +from django.core.exceptions import PermissionDenied, ValidationError +from django.test import TestCase + +from apps.chat.models.chat_message import ChatMessage +from apps.chat.services.message_service import MessageService +from apps.study_groups.models import GroupMember, StudyGroup +from apps.users.models.users import User + + +class TestMessageService(TestCase): + + def setUp(self) -> None: + # 사용자 생성 + self.user = User.objects.create( + email="test@example.com", + name="김오즈", + nickname="테스터", + phone_number="01012345678", + gender="M", + birthday="1999-01-01", + ) + + self.other_user = User.objects.create( + email="hayeong@example.com", + name="송하영", + nickname="ha0", + phone_number="01012341234", + gender="F", + birthday="1997-09-29", + ) + + # 스터디 그룹 생성 + self.group = StudyGroup.objects.create( + name="Django Study", + introduction="스터디 소개", + max_headcount=5, + start_at="2025-12-01T00:00:00Z", + end_at="2025-12-30T00:00:00Z", + ) + + # 그룹 멤버 등록 + GroupMember.objects.create( + study_group_id=self.group, + user_id=self.user, + is_leader=True, + ) + + # 메시지 생성 테스트 + def test_create_message_success(self) -> None: + """그룹 멤버는 정상적으로 메시지 생성 가능""" + + msg = MessageService.create_message(study_group=self.group, user=self.user, content="안녕하세요!") + + self.assertIsInstance(msg, ChatMessage) + self.assertEqual(msg.content, "안녕하세요!") + self.assertEqual(msg.sender_id, self.user.id) + + def test_create_message_fail_for_non_member(self) -> None: + # 그룹 멤버가 아니면 PermissionDenied 발생 + + with self.assertRaises(PermissionDenied): + MessageService.create_message(study_group=self.group, user=self.other_user, content="멤버가 아닙니다") + + def test_create_message_fail_empty_content(self) -> None: + # 빈 메시지 내용이면 ValidationError 발생 + + with self.assertRaises(ValidationError): + MessageService.create_message(study_group=self.group, user=self.user, content=" ") + + # 메시지 단일 조회 테스트 + def test_get_message_success(self) -> None: + """단일 메시지 조회 성공""" + + msg = ChatMessage.objects.create( + study_group=self.group, + sender=self.user, + content="조회 테스트", + ) + + found = MessageService.get_message(msg.id) + + self.assertEqual(found.id, msg.id) + self.assertEqual(found.content, "조회 테스트") + + def test_get_message_not_found(self) -> None: + # 없는 메시지를 조회하면 ValidationError 발생 + + with self.assertRaises(ValidationError): + MessageService.get_message(9999)