test(aicharacter): 크리에이터 회원 연결 검증을 보강한다

This commit is contained in:
2026-06-12 11:39:50 +09:00
parent 268ed751c3
commit 5cf1f7d909
2 changed files with 172 additions and 104 deletions

View File

@@ -0,0 +1,171 @@
package kr.co.vividnext.sodalive.chat.character.service
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberKind
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ContextConfiguration
import org.springframework.transaction.annotation.Transactional
import javax.persistence.EntityManager
@SpringBootTest
@Transactional
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
class ChatCharacterCreatorMemberServiceIntegrationTest @Autowired constructor(
private val creatorMemberService: ChatCharacterCreatorMemberService,
private val memberRepository: MemberRepository,
private val chatCharacterRepository: ChatCharacterRepository,
private val entityManager: EntityManager
) {
@Test
fun `ChatCharacter creatorMember 관계와 repository 메서드를 사용할 수 있다`() {
val member = memberRepository.save(createMember(memberKind = MemberKind.AI_CHARACTER))
val chatCharacter = chatCharacterRepository.save(createCharacter().apply { creatorMember = member })
entityManager.flush()
entityManager.clear()
val found = chatCharacterRepository.findByCreatorMemberId(member.id!!)
assertNotNull(found)
assertEquals(chatCharacter.id, found!!.id)
assertEquals(true, chatCharacterRepository.existsByCreatorMemberId(member.id!!))
}
@Test
fun `AI 캐릭터용 Member를 생성하고 표시 정보를 복사한다`() {
val chatCharacter = createCharacter(
name = "소다",
description = "AI 캐릭터 설명",
imagePath = "characters/1/profile.png"
)
val member = creatorMemberService.ensureAiCharacterCreatorMember(chatCharacter)
chatCharacterRepository.save(chatCharacter)
entityManager.flush()
entityManager.clear()
val savedMember = memberRepository.findById(member.id!!).orElseThrow()
val savedCharacter = chatCharacterRepository.findByCreatorMemberId(member.id!!)
assertNotNull(savedCharacter)
assertNull(savedMember.email)
assertEquals("", savedMember.password)
assertEquals(MemberRole.CREATOR, savedMember.role)
assertEquals(MemberKind.AI_CHARACTER, savedMember.memberKind)
assertEquals("소다", savedMember.nickname)
assertEquals("characters/1/profile.png", savedMember.profileImage)
assertEquals("AI 캐릭터 설명", savedMember.introduce)
}
@Test
fun `AI 캐릭터용 Member 표시 정보를 동기화한다`() {
val member = memberRepository.save(
createMember(memberKind = MemberKind.AI_CHARACTER).apply {
nickname = "old-name"
profileImage = "old/profile.png"
introduce = "old-description"
}
)
val chatCharacter = chatCharacterRepository.save(
createCharacter(
name = "new-name",
description = "new-description",
imagePath = "new/profile.png"
).apply { creatorMember = member }
)
entityManager.flush()
entityManager.clear()
val savedCharacter = chatCharacterRepository.findById(chatCharacter.id!!).orElseThrow()
creatorMemberService.syncAiCharacterCreatorMemberDisplayFields(savedCharacter)
entityManager.flush()
entityManager.clear()
val savedMember = memberRepository.findById(member.id!!).orElseThrow()
assertEquals("new-name", savedMember.nickname)
assertEquals("new/profile.png", savedMember.profileImage)
assertEquals("new-description", savedMember.introduce)
}
@Test
fun `동기화 대상 creatorMember가 없으면 저장 실패를 위해 예외를 던진다`() {
val chatCharacter = createCharacter(
id = 1L,
name = "missing-creator-member",
description = "description"
)
val exception = assertThrows(SodaException::class.java) {
creatorMemberService.syncAiCharacterCreatorMemberDisplayFields(chatCharacter)
}
assertEquals("common.error.invalid_request", exception.messageKey)
}
@Test
fun `사람 크리에이터 Member 표시 정보는 덮어쓰지 않는다`() {
val member = memberRepository.save(
createMember(memberKind = MemberKind.HUMAN).apply {
nickname = "human-name"
profileImage = "human/profile.png"
introduce = "human-description"
}
)
val chatCharacter = chatCharacterRepository.save(
createCharacter(
name = "ai-name",
description = "ai-description",
imagePath = "ai/profile.png"
).apply { creatorMember = member }
)
entityManager.flush()
entityManager.clear()
val savedCharacter = chatCharacterRepository.findById(chatCharacter.id!!).orElseThrow()
creatorMemberService.syncAiCharacterCreatorMemberDisplayFields(savedCharacter)
entityManager.flush()
entityManager.clear()
val savedMember = memberRepository.findById(member.id!!).orElseThrow()
assertEquals("human-name", savedMember.nickname)
assertEquals("human/profile.png", savedMember.profileImage)
assertEquals("human-description", savedMember.introduce)
}
private fun createCharacter(
id: Long? = null,
name: String = "character-name",
description: String = "character-description",
imagePath: String? = null
): ChatCharacter {
val character = ChatCharacter(
characterUUID = "character-uuid-$name",
name = name,
description = description,
systemPrompt = "system-prompt"
)
character.id = id
character.imagePath = imagePath
return character
}
private fun createMember(memberKind: MemberKind): Member {
return Member(
email = if (memberKind == MemberKind.HUMAN) "human-${System.nanoTime()}@example.com" else null,
password = if (memberKind == MemberKind.HUMAN) "password" else "",
nickname = "member-name",
role = MemberRole.CREATOR,
memberKind = memberKind
)
}
}

View File

@@ -7,125 +7,23 @@ import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterHobbyRepo
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterTagRepository import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterTagRepository
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterValueRepository import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterValueRepository
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberKind import kr.co.vividnext.sodalive.member.MemberKind
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertSame
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.Mockito import org.mockito.Mockito
import java.util.Optional import java.util.Optional
class ChatCharacterCreatorMemberServiceTest { class ChatCharacterCreatorMemberServiceTest {
private lateinit var memberRepository: MemberRepository
private lateinit var chatCharacterRepository: ChatCharacterRepository private lateinit var chatCharacterRepository: ChatCharacterRepository
private lateinit var creatorMemberService: ChatCharacterCreatorMemberService private lateinit var creatorMemberService: ChatCharacterCreatorMemberService
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
memberRepository = Mockito.mock(MemberRepository::class.java)
chatCharacterRepository = Mockito.mock(ChatCharacterRepository::class.java) chatCharacterRepository = Mockito.mock(ChatCharacterRepository::class.java)
creatorMemberService = ChatCharacterCreatorMemberService(memberRepository) creatorMemberService = Mockito.mock(ChatCharacterCreatorMemberService::class.java)
}
@Test
fun `ChatCharacter creatorMember 관계와 repository 메서드를 사용할 수 있다`() {
val member = createMember(memberKind = MemberKind.AI_CHARACTER)
val chatCharacter = createCharacter().apply { creatorMember = member }
Mockito.`when`(chatCharacterRepository.findByCreatorMemberId(1L)).thenReturn(chatCharacter)
Mockito.`when`(chatCharacterRepository.existsByCreatorMemberId(1L)).thenReturn(true)
assertSame(member, chatCharacter.creatorMember)
assertSame(chatCharacter, chatCharacterRepository.findByCreatorMemberId(1L))
assertEquals(true, chatCharacterRepository.existsByCreatorMemberId(1L))
}
@Test
fun `AI 캐릭터용 Member를 생성하고 표시 정보를 복사한다`() {
val chatCharacter = createCharacter(
name = "소다",
description = "AI 캐릭터 설명",
imagePath = "characters/1/profile.png"
)
Mockito.`when`(memberRepository.save(Mockito.any(Member::class.java))).thenAnswer { invocation ->
(invocation.arguments[0] as Member).apply { id = 10L }
}
val member = creatorMemberService.ensureAiCharacterCreatorMember(chatCharacter)
assertSame(member, chatCharacter.creatorMember)
assertNull(member.email)
assertEquals("", member.password)
assertEquals(MemberRole.CREATOR, member.role)
assertEquals(MemberKind.AI_CHARACTER, member.memberKind)
assertEquals("소다", member.nickname)
assertEquals("characters/1/profile.png", member.profileImage)
assertEquals("AI 캐릭터 설명", member.introduce)
Mockito.verify(memberRepository).save(member)
}
@Test
fun `AI 캐릭터용 Member 표시 정보를 동기화한다`() {
val member = createMember(memberKind = MemberKind.AI_CHARACTER).apply {
nickname = "old-name"
profileImage = "old/profile.png"
introduce = "old-description"
}
val chatCharacter = createCharacter(
name = "new-name",
description = "new-description",
imagePath = "new/profile.png"
).apply { creatorMember = member }
creatorMemberService.syncAiCharacterCreatorMemberDisplayFields(chatCharacter)
assertEquals("new-name", member.nickname)
assertEquals("new/profile.png", member.profileImage)
assertEquals("new-description", member.introduce)
Mockito.verify(memberRepository).save(member)
}
@Test
fun `동기화 대상 creatorMember가 없으면 저장 실패를 위해 예외를 던진다`() {
val chatCharacter = createCharacter(
id = 1L,
name = "missing-creator-member",
description = "description"
)
val exception = assertThrows(SodaException::class.java) {
creatorMemberService.syncAiCharacterCreatorMemberDisplayFields(chatCharacter)
}
assertEquals("common.error.invalid_request", exception.messageKey)
Mockito.verifyNoInteractions(memberRepository)
}
@Test
fun `사람 크리에이터 Member 표시 정보는 덮어쓰지 않는다`() {
val member = createMember(memberKind = MemberKind.HUMAN).apply {
nickname = "human-name"
profileImage = "human/profile.png"
introduce = "human-description"
}
val chatCharacter = createCharacter(
name = "ai-name",
description = "ai-description",
imagePath = "ai/profile.png"
).apply { creatorMember = member }
creatorMemberService.syncAiCharacterCreatorMemberDisplayFields(chatCharacter)
assertEquals("human-name", member.nickname)
assertEquals("human/profile.png", member.profileImage)
assertEquals("human-description", member.introduce)
Mockito.verifyNoInteractions(memberRepository)
} }
@Test @Test
@@ -172,7 +70,6 @@ class ChatCharacterCreatorMemberServiceTest {
} }
private fun createChatCharacterService(): ChatCharacterService { private fun createChatCharacterService(): ChatCharacterService {
creatorMemberService = Mockito.mock(ChatCharacterCreatorMemberService::class.java)
return ChatCharacterService( return ChatCharacterService(
chatCharacterRepository = chatCharacterRepository, chatCharacterRepository = chatCharacterRepository,
tagRepository = Mockito.mock(ChatCharacterTagRepository::class.java), tagRepository = Mockito.mock(ChatCharacterTagRepository::class.java),