feat(creatorname): 레거시 조회와 검색에 언어별 이름을 적용한다
This commit is contained in:
@@ -18,6 +18,7 @@ import kr.co.vividnext.sodalive.query.recommend.RecommendChannelQueryService
|
||||
import kr.co.vividnext.sodalive.rank.ContentRankingSortType
|
||||
import kr.co.vividnext.sodalive.rank.RankingRepository
|
||||
import kr.co.vividnext.sodalive.rank.RankingService
|
||||
import kr.co.vividnext.sodalive.support.originalLocalizedNameService
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -68,6 +69,7 @@ class HomeServiceTest {
|
||||
rankingRepository = rankingRepository,
|
||||
explorerQueryRepository = explorerQueryRepository,
|
||||
langContext = LangContext().apply { setLang(Lang.JA) },
|
||||
localizedNameService = originalLocalizedNameService(),
|
||||
memberContentPreferenceService = Mockito.mock(
|
||||
kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService::class.java
|
||||
),
|
||||
@@ -99,7 +101,7 @@ class HomeServiceTest {
|
||||
|
||||
Mockito.doReturn(emptyList<Any>()).`when`(liveRoomService)
|
||||
.getRoomList(null, LiveRoomStatus.NOW, Pageable.ofSize(10), null, timezone)
|
||||
Mockito.`when`(rankingRepository.getCreatorRankings(null)).thenReturn(emptyList())
|
||||
Mockito.`when`(rankingRepository.getCreatorRankings(null, Lang.JA)).thenReturn(emptyList())
|
||||
Mockito.`when`(
|
||||
contentThemeService.getActiveThemeOfContent(
|
||||
false,
|
||||
|
||||
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.can
|
||||
|
||||
import kr.co.vividnext.sodalive.can.use.CanUsage
|
||||
import kr.co.vividnext.sodalive.common.CountryContext
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
@@ -22,7 +23,7 @@ class CanServiceTest {
|
||||
fun setUp() {
|
||||
repository = mock(CanRepository::class.java)
|
||||
countryContext = mock(CountryContext::class.java)
|
||||
service = CanService(repository, countryContext)
|
||||
service = CanService(repository, countryContext, LangContext())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
import kr.co.vividnext.sodalive.admin.content.series.genre.SeriesGenre
|
||||
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration
|
||||
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationItem
|
||||
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCurationQueryRepository
|
||||
import kr.co.vividnext.sodalive.content.order.Order
|
||||
import kr.co.vividnext.sodalive.content.order.OrderType
|
||||
import kr.co.vividnext.sodalive.content.series.ContentSeriesRepository
|
||||
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslation
|
||||
import kr.co.vividnext.sodalive.content.series.translation.SeriesTranslationPayload
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
|
||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslation
|
||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslationPayload
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.Series
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesContent
|
||||
import kr.co.vividnext.sodalive.explorer.CreatorRanking
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMember
|
||||
import kr.co.vividnext.sodalive.rank.RankingRepository
|
||||
import org.junit.jupiter.api.Assertions.assertAll
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotEquals
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.jdbc.core.JdbcTemplate
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@DataJpaTest(
|
||||
properties = [
|
||||
"spring.cache.type=none",
|
||||
"spring.datasource.url=jdbc:h2:mem:legacy-content-owner-name;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE"
|
||||
]
|
||||
)
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||||
@Import(QueryDslConfig::class, RankingRepository::class, AudioContentCurationQueryRepository::class)
|
||||
class AudioContentRepositoryLocalizedNameTest @Autowired constructor(
|
||||
private val repository: AudioContentRepository,
|
||||
private val seriesRepository: ContentSeriesRepository,
|
||||
private val rankingRepository: RankingRepository,
|
||||
private val curationRepository: AudioContentCurationQueryRepository,
|
||||
private val entityManager: EntityManager,
|
||||
private val jdbcTemplate: JdbcTemplate
|
||||
) {
|
||||
@Test
|
||||
@DisplayName("JA 메인 콘텐츠 조회는 콘텐츠 ID가 아닌 실제 소유자 ID로 크리에이터 이름만 대체한다")
|
||||
fun shouldLocalizeCurrentOwnerNameByCreatorIdWithoutChangingContentTitleOrUserName() {
|
||||
val viewer = saveMember("owner-query-viewer", MemberRole.USER)
|
||||
val creator = saveMember("owner-original", MemberRole.CREATOR)
|
||||
val user = saveMember("user-original", MemberRole.USER)
|
||||
val creatorContent = saveContent(creator, "Original audio title").takeIf { it.id != creator.id }
|
||||
?: saveContent(creator, "Original audio title")
|
||||
val userContent = saveContent(user, "User audio title")
|
||||
saveAlias(creator, "Japanese owner")
|
||||
saveAlias(user, "Must not replace USER")
|
||||
entityManager.clear()
|
||||
assertNotEquals(creator.id, creatorContent.id)
|
||||
|
||||
val items = findLatest(viewer.id!!, "ja").associateBy { it.contentId }
|
||||
|
||||
assertAll(
|
||||
{ assertEquals("Japanese owner", items[creatorContent.id]!!.creatorNickname) },
|
||||
{ assertEquals(creator.id, items[creatorContent.id]!!.creatorId) },
|
||||
{ assertEquals("Original audio title", items[creatorContent.id]!!.title) },
|
||||
{ assertEquals("user-original", items[userContent.id]!!.creatorNickname) },
|
||||
{ assertEquals("User audio title", items[userContent.id]!!.title) },
|
||||
{
|
||||
assertEquals(
|
||||
"owner-original",
|
||||
findLatest(viewer.id!!, "ko").first { it.contentId == creatorContent.id }.creatorNickname
|
||||
)
|
||||
},
|
||||
{
|
||||
assertEquals(
|
||||
"owner-original",
|
||||
findLatest(viewer.id!!, "en").first { it.contentId == creatorContent.id }.creatorNickname
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("메인 콘텐츠 이름 대체는 공개 필터를 유지하고 해제 후 변경된 현재 원본으로 돌아간다")
|
||||
fun shouldPreserveVisibilityFiltersAndFallbackToRenamedOriginalAfterSoftClear() {
|
||||
val viewer = saveMember("visibility-query-viewer", MemberRole.USER)
|
||||
val creator = saveMember("before-rename", MemberRole.CREATOR)
|
||||
val visible = saveContent(creator, "visible title")
|
||||
saveContent(creator, "future title").releaseDate = LocalDateTime.now().plusDays(1)
|
||||
saveContent(creator, "adult title").isAdult = true
|
||||
saveContent(creator, "inactive title").isActive = false
|
||||
saveContent(creator, "unfinished title").duration = null
|
||||
val blockedCreator = saveMember("blocked-owner", MemberRole.CREATOR)
|
||||
saveContent(blockedCreator, "blocked title")
|
||||
entityManager.persist(
|
||||
BlockMember(isActive = true).apply {
|
||||
member = blockedCreator
|
||||
blockedMember = viewer
|
||||
}
|
||||
)
|
||||
saveAlias(creator, "Retained owner alias")
|
||||
saveAlias(blockedCreator, "Blocked owner alias")
|
||||
creator.nickname = "renamed-original"
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
val activeItems = findLatest(viewer.id!!, "ja")
|
||||
assertEquals(listOf(visible.id), activeItems.map { it.contentId })
|
||||
assertEquals("Retained owner alias", activeItems.single().creatorNickname)
|
||||
|
||||
jdbcTemplate.update(
|
||||
"update creator_localized_name set is_active = false where member_id = ? and lang = 'JA'",
|
||||
creator.id!!
|
||||
)
|
||||
entityManager.clear()
|
||||
assertEquals("renamed-original", findLatest(viewer.id!!, "ja").single().creatorNickname)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("테마 공유 조회는 요청한 KO·JA 별칭과 기존 제목을 반환한다")
|
||||
fun shouldLocalizeThemeNamesByRequestedLanguageWithoutChangingTitle() {
|
||||
val viewer = saveMember("raw-theme-viewer", MemberRole.USER)
|
||||
val creator = saveMember("raw-theme-original", MemberRole.CREATOR)
|
||||
val content = saveContent(creator, "Unchanged theme title")
|
||||
saveAlias(creator, "Japanese theme owner")
|
||||
saveAlias(creator, "Korean theme owner", Lang.KO)
|
||||
entityManager.clear()
|
||||
|
||||
val names = listOf(
|
||||
Lang.KO to "Korean theme owner",
|
||||
Lang.JA to "Japanese theme owner"
|
||||
)
|
||||
for ((lang, expected) in names) {
|
||||
val item = repository.findByTheme(memberId = viewer.id!!, contentType = ContentType.ALL, lang = lang).single()
|
||||
assertEquals(content.id, item.contentId)
|
||||
assertEquals(expected, item.creatorNickname)
|
||||
assertEquals("Unchanged theme title", item.title)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("큐레이션 조회는 활성 KO·JA 별칭이 있어도 원본 닉네임을 반환한다")
|
||||
fun shouldKeepCurationNamesRawWithActiveAliases() {
|
||||
val creator = saveMember("raw-curation-original", MemberRole.CREATOR)
|
||||
val curation = AudioContentCuration(title = "curation", description = "description")
|
||||
.also { entityManager.persist(it) }
|
||||
val content = saveContent(creator, "Unchanged curation title").apply { this.curation = curation }
|
||||
entityManager.persist(
|
||||
AudioContentCurationItem().apply {
|
||||
this.curation = curation
|
||||
this.content = content
|
||||
}
|
||||
)
|
||||
saveAlias(creator, "Japanese curation owner")
|
||||
saveAlias(creator, "Korean curation owner", Lang.KO)
|
||||
entityManager.clear()
|
||||
|
||||
val results = listOf(
|
||||
repository.findContentByCurationId(curation.id!!, false, ContentType.ALL),
|
||||
curationRepository.findByCurationId(curation.id!!, "https://cdn.test", contentType = ContentType.ALL)
|
||||
)
|
||||
for (items in results) {
|
||||
assertEquals(content.id, items.single().contentId)
|
||||
assertEquals("raw-curation-original", items.single().creatorNickname)
|
||||
assertEquals("Unchanged curation title", items.single().title)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("랭킹 공유 조회는 요청한 KO·JA 별칭과 제목 번역·원본 fallback을 반환한다")
|
||||
fun shouldLocalizeRankingNamesAndTitlesByRequestedLocale() {
|
||||
val viewer = saveMember("raw-rank-viewer", MemberRole.USER)
|
||||
val creator = saveMember("raw-rank-original", MemberRole.CREATOR).apply { profileImage = "profile.png" }
|
||||
val content = saveContent(creator, "Original ranking title")
|
||||
entityManager.persist(CreatorRanking(ranking = 1).apply { member = creator })
|
||||
entityManager.persist(
|
||||
Order(type = OrderType.KEEP).apply {
|
||||
member = viewer
|
||||
this.creator = creator
|
||||
audioContent = content
|
||||
}
|
||||
)
|
||||
entityManager.persist(
|
||||
ContentTranslation(content.id!!, "ja", ContentTranslationPayload("Translated ranking title", "detail", ""))
|
||||
)
|
||||
saveAlias(creator, "Japanese ranking owner")
|
||||
saveAlias(creator, "Korean ranking owner", Lang.KO)
|
||||
// H2 JSON 컬럼에 문자열이 아닌 JSON 객체를 적재한다.
|
||||
jdbcTemplate.update(
|
||||
"update content_translation set rendered_payload = ? format json where content_id = ? and locale = 'ja'",
|
||||
"""{"title":"Translated ranking title","detail":"detail","tags":""}""",
|
||||
content.id!!
|
||||
)
|
||||
entityManager.clear()
|
||||
|
||||
val names = listOf(
|
||||
Triple(Lang.KO, "Korean ranking owner", "Original ranking title"),
|
||||
Triple(Lang.JA, "Japanese ranking owner", "Translated ranking title")
|
||||
)
|
||||
for ((lang, expected, expectedTitle) in names) {
|
||||
assertEquals(expected, rankingRepository.getCreatorRankings(viewer.id, lang).single().nickname)
|
||||
val item = rankingRepository.getAudioContentRanking(
|
||||
memberId = viewer.id,
|
||||
isAdult = false,
|
||||
contentType = ContentType.ALL,
|
||||
startDate = LocalDateTime.now().minusDays(1),
|
||||
endDate = LocalDateTime.now().plusDays(1),
|
||||
offset = 0,
|
||||
limit = 10,
|
||||
sortType = "판매량",
|
||||
locale = lang.code
|
||||
).single()
|
||||
assertEquals(expected, item.creatorNickname)
|
||||
assertEquals(expectedTitle, item.title)
|
||||
}
|
||||
assertEquals(
|
||||
"Korean ranking owner",
|
||||
rankingRepository.getCreatorRankings(viewer.id).single().nickname
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("오리지널 시리즈 공유 조회는 요청한 언어의 제목 번역·원본 fallback을 반환한다")
|
||||
fun shouldLocalizeOriginalSeriesTitlesByRequestedLocale() {
|
||||
val creator = saveMember("raw-series-original", MemberRole.CREATOR)
|
||||
val content = saveContent(creator, "Series content")
|
||||
val genre = SeriesGenre(genre = "genre").also { entityManager.persist(it) }
|
||||
val series = Series(title = "Original series title", introduction = "introduction", isOriginal = true).apply {
|
||||
member = creator
|
||||
this.genre = genre
|
||||
coverImage = "series.png"
|
||||
entityManager.persist(this)
|
||||
}
|
||||
entityManager.persist(
|
||||
SeriesContent().apply {
|
||||
this.series = series
|
||||
this.content = content
|
||||
}
|
||||
)
|
||||
entityManager.persist(
|
||||
SeriesTranslation(series.id!!, "ja", SeriesTranslationPayload("Translated series title", "intro", emptyList()))
|
||||
)
|
||||
entityManager.flush()
|
||||
// H2 JSON 컬럼에 문자열이 아닌 JSON 객체를 적재한다.
|
||||
jdbcTemplate.update(
|
||||
"update series_translation set rendered_payload = ? format json where series_id = ? and locale = 'ja'",
|
||||
"""{"title":"Translated series title","introduction":"intro","keywords":[]}""",
|
||||
series.id!!
|
||||
)
|
||||
entityManager.clear()
|
||||
|
||||
val titles = listOf(
|
||||
"ja" to "Translated series title",
|
||||
"ko" to "Original series title"
|
||||
)
|
||||
for ((locale, expectedTitle) in titles) {
|
||||
val localized = seriesRepository.getOriginalAudioDramaList(
|
||||
imageHost = "https://cdn.test",
|
||||
isAdult = false,
|
||||
contentType = ContentType.ALL,
|
||||
locale = locale
|
||||
).single()
|
||||
assertEquals(expectedTitle, localized.title)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findLatest(viewerId: Long, locale: String): List<AudioContentMainItem> {
|
||||
return repository.getLatestContentByTheme(
|
||||
memberId = viewerId,
|
||||
theme = emptyList(),
|
||||
contentType = ContentType.ALL,
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
sortType = SortType.NEWEST,
|
||||
isFree = false,
|
||||
isAdult = false,
|
||||
locale = locale
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveMember(nickname: String, role: MemberRole): Member {
|
||||
return Member(
|
||||
email = "$nickname@test.com",
|
||||
password = "password",
|
||||
nickname = nickname,
|
||||
profileImage = "profile.png",
|
||||
role = role
|
||||
).also {
|
||||
entityManager.persist(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveContent(creator: Member, title: String): AudioContent {
|
||||
val theme = AudioContentTheme(theme = "theme-$title", image = "theme.png")
|
||||
entityManager.persist(theme)
|
||||
return AudioContent(
|
||||
title = title,
|
||||
detail = "detail",
|
||||
languageCode = "ko",
|
||||
releaseDate = LocalDateTime.now().minusDays(1)
|
||||
).apply {
|
||||
member = creator
|
||||
this.theme = theme
|
||||
isActive = true
|
||||
coverImage = "audio.png"
|
||||
duration = "00:10:00"
|
||||
entityManager.persist(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAlias(member: Member, displayName: String, lang: Lang = Lang.JA) {
|
||||
entityManager.flush()
|
||||
jdbcTemplate.update(
|
||||
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
|
||||
"values (?, ?, ?, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
||||
member.id!!,
|
||||
lang.name,
|
||||
displayName
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.support.originalLocalizedNameService
|
||||
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.application.CreatorContentViewHistoryService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@@ -114,6 +115,7 @@ class AudioContentServiceTest {
|
||||
applicationEventPublisher = applicationEventPublisher,
|
||||
messageSource = SodaMessageSource(),
|
||||
langContext = LangContext(),
|
||||
localizedNameService = originalLocalizedNameService(),
|
||||
contentThemeTranslationRepository = contentThemeTranslationRepository,
|
||||
homeFollowingNewsPublishService = homeFollowingNewsPublishService,
|
||||
memberContentPreferenceService = memberContentPreferenceService,
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package kr.co.vividnext.sodalive.content.comment
|
||||
|
||||
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||
import kr.co.vividnext.sodalive.content.AudioContent
|
||||
import kr.co.vividnext.sodalive.content.AudioContentRepository
|
||||
import kr.co.vividnext.sodalive.content.order.OrderRepository
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
|
||||
import kr.co.vividnext.sodalive.fcm.FcmEvent
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
import kr.co.vividnext.sodalive.support.LegacyCreatorNameTestConfiguration
|
||||
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotEquals
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.CsvSource
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.Mockito
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
import org.springframework.context.annotation.Import
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@DataJpaTest(properties = ["spring.cache.type=none"])
|
||||
@Import(QueryDslConfig::class, LegacyCreatorNameTestConfiguration::class)
|
||||
class AudioContentCommentNameMetadataTest @Autowired constructor(
|
||||
private val entityManager: EntityManager,
|
||||
private val repository: AudioContentCommentRepository,
|
||||
private val blockRepository: BlockMemberRepository,
|
||||
private val contentRepository: AudioContentRepository,
|
||||
private val orderRepository: OrderRepository,
|
||||
private val localizedNameService: CreatorLocalizedNameService
|
||||
) {
|
||||
@ParameterizedTest
|
||||
@CsvSource("CREATOR, USER", "USER, CREATOR")
|
||||
@DisplayName("답글 알림 제목은 부모 작성자 ID이고 snapshot 발신자 ID와 콘텐츠 제목 인자는 그대로다")
|
||||
fun shouldKeepParentTitleOwnerSeparateFromActualCommenterAndResourceTitle(
|
||||
parentRole: MemberRole,
|
||||
commenterRole: MemberRole
|
||||
) {
|
||||
val parentWriter = Member(nickname = "parent-original", password = "password", role = parentRole)
|
||||
val commenter = Member(nickname = "commenter-original", password = "password", role = commenterRole)
|
||||
val contentOwner = Member(nickname = "content-owner-original", password = "password", role = MemberRole.CREATOR)
|
||||
entityManager.persist(parentWriter)
|
||||
entityManager.persist(commenter)
|
||||
entityManager.persist(contentOwner)
|
||||
val theme = AudioContentTheme(theme = "metadata-theme", image = "theme.png")
|
||||
entityManager.persist(theme)
|
||||
val content = AudioContent(title = "Unchanged content title", detail = "detail", languageCode = "ko").apply {
|
||||
member = contentOwner
|
||||
this.theme = theme
|
||||
isActive = true
|
||||
entityManager.persist(this)
|
||||
}
|
||||
val parent = AudioContentComment(comment = "parent comment", languageCode = "ko").apply {
|
||||
member = parentWriter
|
||||
audioContent = content
|
||||
entityManager.persist(this)
|
||||
}
|
||||
entityManager.flush()
|
||||
val publisher = Mockito.mock(ApplicationEventPublisher::class.java)
|
||||
val service = AudioContentCommentService(
|
||||
repository,
|
||||
blockRepository,
|
||||
contentRepository,
|
||||
publisher,
|
||||
orderRepository,
|
||||
SodaMessageSource(),
|
||||
LangContext(),
|
||||
localizedNameService,
|
||||
"https://cdn.test"
|
||||
)
|
||||
|
||||
service.registerComment(commenter, "reply", content.id!!, parent.id!!, false, "ko")
|
||||
|
||||
val captor = ArgumentCaptor.forClass(FcmEvent::class.java)
|
||||
Mockito.verify(publisher).publishEvent(captor.capture())
|
||||
val event = captor.value
|
||||
assertNotEquals(parentWriter.id, commenter.id)
|
||||
assertEquals(parentWriter.id, event.titleMemberId)
|
||||
assertEquals(commenter.id, event.senderMemberId)
|
||||
assertEquals("parent-original", event.title)
|
||||
assertEquals(listOf("Unchanged content title"), event.args)
|
||||
assertEquals(emptyMap<Int, Long>(), event.nameArgumentMemberIds)
|
||||
}
|
||||
}
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
package kr.co.vividnext.sodalive.explorer
|
||||
|
||||
import kr.co.vividnext.sodalive.admin.content.series.genre.SeriesGenre
|
||||
import kr.co.vividnext.sodalive.common.CountryContext
|
||||
import kr.co.vividnext.sodalive.content.AudioContent
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.Series
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesContent
|
||||
import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberAdapter
|
||||
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.assertNotEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.mock.mockito.MockBean
|
||||
import org.springframework.jdbc.core.JdbcTemplate
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import org.springframework.test.context.transaction.TestTransaction
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@SpringBootTest(
|
||||
properties = [
|
||||
"cloud.aws.cloud-front.host=https://cdn.test",
|
||||
"spring.cache.type=none",
|
||||
"spring.datasource.url=jdbc:h2:mem:legacy-creator-name-e2e;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE"
|
||||
]
|
||||
)
|
||||
@AutoConfigureMockMvc
|
||||
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
|
||||
@Transactional
|
||||
class ExplorerLocalizedNameEndToEndTest @Autowired constructor(
|
||||
private val mockMvc: MockMvc,
|
||||
private val entityManager: EntityManager,
|
||||
private val jdbcTemplate: JdbcTemplate
|
||||
) {
|
||||
@MockBean
|
||||
private lateinit var countryContext: CountryContext
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
Mockito.doReturn("US").`when`(countryContext).countryCode
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("레거시 프로필 이름은 현지화하되 팬톡 작성자·답글은 활성 JA·KO 별칭이 있어도 원본을 유지한다")
|
||||
fun shouldLocalizeProfileButKeepOriginalFanTalkWriterAndReplyNamesDespiteActiveAliases() {
|
||||
val viewer = saveMember("profile-viewer", MemberRole.USER)
|
||||
val creator = saveMember("profile-original", MemberRole.CREATOR).apply { isVisibleDonationRank = false }
|
||||
val fan = saveMember("fan-original", MemberRole.USER)
|
||||
val peer = saveMember("deleted_peer-original", MemberRole.CREATOR)
|
||||
val fanTalk = saveCheers(fan, creator, "fan talk")
|
||||
saveCheers(peer, creator, "creator fan talk")
|
||||
saveCheers(creator, creator, "creator reply", parent = fanTalk)
|
||||
saveAlias(creator, "Japanese profile")
|
||||
saveAlias(peer, "Japanese peer")
|
||||
saveAlias(fan, "Must not replace fan")
|
||||
saveAlias(creator, "Korean profile", lang = "KO")
|
||||
saveAlias(peer, "Korean peer", lang = "KO")
|
||||
entityManager.clear()
|
||||
commitFixture()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/explorer/profile/${creator.id}")
|
||||
.param("timezone", "Asia/Seoul")
|
||||
.header("Accept-Language", "ja")
|
||||
.with(user(MemberAdapter(viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.creator.creatorId").value(creator.id!!))
|
||||
.andExpect(jsonPath("$.data.creator.nickname").value("Japanese profile"))
|
||||
.andExpect(jsonPath("$.data.cheers.totalCount").value(2))
|
||||
.andExpect(jsonPath("$.data.cheers.cheers[0].memberId").value(peer.id!!))
|
||||
.andExpect(jsonPath("$.data.cheers.cheers[0].nickname").value("peer-original"))
|
||||
.andExpect(jsonPath("$.data.cheers.cheers[1].nickname").value("fan-original"))
|
||||
.andExpect(jsonPath("$.data.cheers.cheers[1].replyList[0].memberId").value(creator.id!!))
|
||||
.andExpect(jsonPath("$.data.cheers.cheers[1].replyList[0].nickname").value("profile-original"))
|
||||
|
||||
listOf("ko", "en").forEach { locale ->
|
||||
mockMvc.perform(
|
||||
get("/explorer/profile/${creator.id}")
|
||||
.param("timezone", "Asia/Seoul")
|
||||
.header("Accept-Language", locale)
|
||||
.with(user(MemberAdapter(viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(
|
||||
jsonPath("$.data.creator.nickname").value(if (locale == "ko") "Korean profile" else "profile-original")
|
||||
)
|
||||
.andExpect(jsonPath("$.data.cheers.cheers[0].nickname").value("peer-original"))
|
||||
.andExpect(jsonPath("$.data.cheers.cheers[1].nickname").value("fan-original"))
|
||||
.andExpect(jsonPath("$.data.cheers.cheers[1].replyList[0].nickname").value("profile-original"))
|
||||
}
|
||||
assertEquals("profile-original", entityManager.find(Member::class.java, creator.id).nickname)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("JA 상세는 원본 변경 후에도 별칭을 유지하고 해제·미등록이면 현재 원본을 반환한다")
|
||||
fun shouldKeepAliasAfterOriginalRenameAndFallbackAfterSoftClearOrMissingAlias() {
|
||||
val viewer = saveMember("detail-viewer", MemberRole.USER)
|
||||
val creator = saveMember("before-rename", MemberRole.CREATOR)
|
||||
val unregistered = saveMember("unregistered-original", MemberRole.CREATOR)
|
||||
saveAlias(creator, "Retained profile alias")
|
||||
creator.nickname = "renamed-original"
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
commitFixture()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/explorer/profile/${creator.id}/detail")
|
||||
.header("Accept-Language", "ja")
|
||||
.with(user(MemberAdapter(viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.nickname").value("Retained profile alias"))
|
||||
|
||||
jdbcTemplate.update(
|
||||
"update creator_localized_name set is_active = false where member_id = ? and lang = 'JA'",
|
||||
creator.id!!
|
||||
)
|
||||
entityManager.clear()
|
||||
mockMvc.perform(
|
||||
get("/explorer/profile/${creator.id}/detail")
|
||||
.header("Accept-Language", "ja")
|
||||
.with(user(MemberAdapter(viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.nickname").value("renamed-original"))
|
||||
mockMvc.perform(
|
||||
get("/explorer/profile/${unregistered.id}/detail")
|
||||
.header("Accept-Language", "ja")
|
||||
.with(user(MemberAdapter(viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.nickname").value("unregistered-original"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("통합 검색은 creator 제목·닉네임만 JA 이름으로 바꾸고 콘텐츠·시리즈 제목과 소유자 ID를 구분한다")
|
||||
fun shouldLocalizeCreatorSearchTitleAndAllOwnerNamesWithoutReplacingContentOrSeriesTitles() {
|
||||
val viewer = saveMember("unified-viewer", MemberRole.USER)
|
||||
saveMember("unused-member", MemberRole.USER)
|
||||
val creator = saveMember("unified-original", MemberRole.CREATOR)
|
||||
val theme = AudioContentTheme(theme = "unified-theme", image = "theme.png")
|
||||
entityManager.persist(theme)
|
||||
val content = AudioContent(
|
||||
title = "Unchanged content title",
|
||||
detail = "description",
|
||||
languageCode = "ko",
|
||||
releaseDate = LocalDateTime.now().minusDays(1)
|
||||
).apply {
|
||||
member = creator
|
||||
this.theme = theme
|
||||
isActive = true
|
||||
coverImage = "audio.png"
|
||||
duration = "00:10:00"
|
||||
entityManager.persist(this)
|
||||
}
|
||||
val genre = SeriesGenre(genre = "unified-genre", isAdult = false, isActive = true)
|
||||
entityManager.persist(genre)
|
||||
val series = Series(title = "Unchanged series title", introduction = "introduction").apply {
|
||||
member = creator
|
||||
this.genre = genre
|
||||
coverImage = "series.png"
|
||||
entityManager.persist(this)
|
||||
}
|
||||
entityManager.persist(
|
||||
SeriesContent().apply {
|
||||
this.series = series
|
||||
this.content = content
|
||||
}
|
||||
)
|
||||
saveAlias(creator, "Japanese unified creator")
|
||||
entityManager.clear()
|
||||
assertNotEquals(creator.id, content.id)
|
||||
assertNotEquals(creator.id, series.id)
|
||||
commitFixture()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/search")
|
||||
.param("keyword", "unified-original")
|
||||
.header("Accept-Language", "ja")
|
||||
.with(user(MemberAdapter(viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.creatorList[0].id").value(creator.id!!))
|
||||
.andExpect(jsonPath("$.data.creatorList[0].title").value("Japanese unified creator"))
|
||||
.andExpect(jsonPath("$.data.creatorList[0].nickname").value("Japanese unified creator"))
|
||||
.andExpect(jsonPath("$.data.contentList[0].id").value(content.id!!))
|
||||
.andExpect(jsonPath("$.data.contentList[0].title").value("Unchanged content title"))
|
||||
.andExpect(jsonPath("$.data.contentList[0].nickname").value("Japanese unified creator"))
|
||||
.andExpect(jsonPath("$.data.seriesList[0].id").value(series.id!!))
|
||||
.andExpect(jsonPath("$.data.seriesList[0].title").value("Unchanged series title"))
|
||||
.andExpect(jsonPath("$.data.seriesList[0].nickname").value("Japanese unified creator"))
|
||||
|
||||
mockMvc.perform(
|
||||
get("/search/creators")
|
||||
.param("keyword", "Japanese unified")
|
||||
.header("Accept-Language", "ko")
|
||||
.with(user(MemberAdapter(viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.totalCount").value(1))
|
||||
.andExpect(jsonPath("$.data.items[0].title").value("unified-original"))
|
||||
.andExpect(jsonPath("$.data.items[0].nickname").value("unified-original"))
|
||||
}
|
||||
|
||||
private fun commitFixture() {
|
||||
TestTransaction.flagForCommit()
|
||||
TestTransaction.end()
|
||||
TestTransaction.start()
|
||||
}
|
||||
|
||||
private fun saveMember(nickname: String, role: MemberRole): Member {
|
||||
return Member(
|
||||
email = "$nickname@test.com",
|
||||
password = "password",
|
||||
nickname = nickname,
|
||||
profileImage = "$nickname.png",
|
||||
role = role
|
||||
).also { entityManager.persist(it) }
|
||||
}
|
||||
|
||||
private fun saveCheers(writer: Member, creator: Member, text: String, parent: CreatorCheers? = null): CreatorCheers {
|
||||
return CreatorCheers(cheers = text, languageCode = "ko").apply {
|
||||
member = writer
|
||||
this.creator = creator
|
||||
this.parent = parent
|
||||
entityManager.persist(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAlias(member: Member, displayName: String, lang: String = "JA") {
|
||||
entityManager.flush()
|
||||
jdbcTemplate.update(
|
||||
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
|
||||
"values (?, ?, ?, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
||||
member.id!!,
|
||||
lang,
|
||||
displayName
|
||||
)
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
package kr.co.vividnext.sodalive.explorer
|
||||
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import org.junit.jupiter.api.Assertions.assertAll
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.jdbc.core.JdbcTemplate
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@DataJpaTest(properties = ["spring.cache.type=none"])
|
||||
@Import(QueryDslConfig::class)
|
||||
class ExplorerQueryRepositoryLocalizedNameTest @Autowired constructor(
|
||||
private val entityManager: EntityManager,
|
||||
private val jdbcTemplate: JdbcTemplate,
|
||||
queryFactory: JPAQueryFactory
|
||||
) {
|
||||
private val langContext = LangContext().apply { setLang(Lang.KO) }
|
||||
private val repository = ExplorerQueryRepository(queryFactory, SodaMessageSource(), langContext, "https://cdn.test")
|
||||
|
||||
@Test
|
||||
@DisplayName("KO 채널 검색도 원본 또는 활성 JA 이름을 찾고 self·비활성·USER를 제외한다")
|
||||
fun shouldSearchOriginalOrActiveJaAliasInKoWithoutBypassingChannelFilters() {
|
||||
val viewer = saveMember("Comet-viewer")
|
||||
val original = saveMember("Comet-original")
|
||||
val aliasOnly = saveMember("another-original")
|
||||
val inactive = saveMember("Comet-inactive").apply { isActive = false }
|
||||
val user = saveMember("Comet-user", MemberRole.USER)
|
||||
listOf(viewer, original, aliasOnly, inactive, user).forEach { saveAlias(it, "Comet alias") }
|
||||
entityManager.clear()
|
||||
|
||||
assertAll(
|
||||
{
|
||||
assertEquals(
|
||||
setOf(original.id, aliasOnly.id),
|
||||
repository.getSearchChannel("cOmEt", viewer.id!!).map { it.id }.toSet()
|
||||
)
|
||||
},
|
||||
{ assertEquals(2, repository.getSearchChannel("cOmEt", viewer.id!!).size) },
|
||||
{ assertEquals(listOf(original.id), repository.getSearchChannel("Comet-original", viewer.id!!).map { it.id }) }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("채널 검색은 해제한 JA와 EN 이름을 제외하고 동일 행 재활성화 후 별칭을 다시 찾는다")
|
||||
fun shouldIgnoreSoftClearedAndNonJaChannelAliasesAndSearchReactivatedAlias() {
|
||||
val viewer = saveMember("channel-viewer", MemberRole.USER)
|
||||
val creator = saveMember("channel-original")
|
||||
saveAlias(creator, "ChannelAlias", isActive = false)
|
||||
saveAlias(creator, "EnglishChannel", lang = "EN")
|
||||
entityManager.clear()
|
||||
|
||||
assertEquals(emptyList<Long>(), repository.getSearchChannel("ChannelAlias", viewer.id!!).map { it.id })
|
||||
assertEquals(emptyList<Long>(), repository.getSearchChannel("EnglishChannel", viewer.id!!).map { it.id })
|
||||
assertEquals(listOf(creator.id), repository.getSearchChannel("channel-original", viewer.id!!).map { it.id })
|
||||
jdbcTemplate.update(
|
||||
"update creator_localized_name set is_active = true where member_id = ? and lang = 'JA'",
|
||||
creator.id!!
|
||||
)
|
||||
|
||||
assertEquals(listOf(creator.id), repository.getSearchChannel("ChannelAlias", viewer.id!!).map { it.id })
|
||||
}
|
||||
|
||||
private fun saveMember(nickname: String, role: MemberRole = MemberRole.CREATOR): Member {
|
||||
return Member(email = "$nickname@test.com", password = "password", nickname = nickname, role = role).also {
|
||||
entityManager.persist(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAlias(member: Member, displayName: String, isActive: Boolean = true, lang: String = "JA") {
|
||||
entityManager.flush()
|
||||
jdbcTemplate.update(
|
||||
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
|
||||
"values (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
||||
member.id!!,
|
||||
lang,
|
||||
displayName,
|
||||
isActive
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,15 @@ import kr.co.vividnext.sodalive.explorer.profile.CreatorDonationRankingService
|
||||
import kr.co.vividnext.sodalive.explorer.profile.PutWriteCheersRequest
|
||||
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationService
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityService
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.MemberService
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -42,7 +45,28 @@ class ExplorerServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createService(queryRepository: ExplorerQueryRepository): ExplorerService {
|
||||
@Test
|
||||
@DisplayName("팬톡 수정 응답은 별칭 대신 작성자 원본 이름을 반환하고 deleted_ 접두사만 제거한다")
|
||||
fun shouldKeepOriginalWriterNameAndRemoveDeletedPrefixWhenModifyingCheers() {
|
||||
val queryRepository = Mockito.mock(ExplorerQueryRepository::class.java)
|
||||
val localizedNameService = Mockito.mock(CreatorLocalizedNameService::class.java)
|
||||
val service = createService(queryRepository, localizedNameService)
|
||||
val writer = createMember(id = 10L, nickname = "deleted_writer", role = MemberRole.CREATOR)
|
||||
val cheers = createCheers(writer, createMember(id = 20L, nickname = "creator", role = MemberRole.CREATOR))
|
||||
Mockito.doReturn(cheers).`when`(queryRepository).getCheers(101L)
|
||||
Mockito.`when`(localizedNameService.findLocalizedNames(listOf(10L), Lang.JA)).thenReturn(mapOf(10L to "alias"))
|
||||
|
||||
val response = service.modifyCheers(PutWriteCheersRequest(cheersId = 101L, content = "modified"), writer)
|
||||
|
||||
assertEquals("writer", response.writerNickname)
|
||||
assertEquals("modified", response.content)
|
||||
Mockito.verifyNoInteractions(localizedNameService)
|
||||
}
|
||||
|
||||
private fun createService(
|
||||
queryRepository: ExplorerQueryRepository,
|
||||
localizedNameService: CreatorLocalizedNameService = Mockito.mock(CreatorLocalizedNameService::class.java)
|
||||
): ExplorerService {
|
||||
return ExplorerService(
|
||||
memberService = Mockito.mock(MemberService::class.java),
|
||||
memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java),
|
||||
@@ -57,7 +81,8 @@ class ExplorerServiceTest {
|
||||
applicationEventPublisher = Mockito.mock(ApplicationEventPublisher::class.java),
|
||||
contentTranslationRepository = Mockito.mock(ContentTranslationRepository::class.java),
|
||||
messageSource = Mockito.mock(SodaMessageSource::class.java),
|
||||
langContext = Mockito.mock(LangContext::class.java),
|
||||
langContext = LangContext().apply { setLang(Lang.JA) },
|
||||
localizedNameService = localizedNameService,
|
||||
cloudFrontHost = "https://cdn.test"
|
||||
)
|
||||
}
|
||||
|
||||
+10
-9
@@ -72,9 +72,9 @@ class ChannelDonationMessageRepositoryTest @Autowired constructor(
|
||||
|
||||
// then: 비공개 타인 메시지는 제외되고, createdAt desc/id desc 정렬이 적용돼야 한다.
|
||||
assertEquals(3, list.size)
|
||||
assertEquals(secretMine.id, list[0].id)
|
||||
assertEquals(publicTieSecond.id, list[1].id)
|
||||
assertEquals(publicTieFirst.id, list[2].id)
|
||||
assertEquals(secretMine.id, list[0].first.id)
|
||||
assertEquals(publicTieSecond.id, list[1].first.id)
|
||||
assertEquals(publicTieFirst.id, list[2].first.id)
|
||||
assertEquals(3, totalCount)
|
||||
}
|
||||
|
||||
@@ -127,9 +127,9 @@ class ChannelDonationMessageRepositoryTest @Autowired constructor(
|
||||
|
||||
// then: 본인 조회는 비공개 메시지를 포함하고 최신순으로 반환해야 한다.
|
||||
assertEquals(3, list.size)
|
||||
assertEquals(recentSecretOther.id, list[0].id)
|
||||
assertEquals(recentSecretMine.id, list[1].id)
|
||||
assertEquals(recentPublic.id, list[2].id)
|
||||
assertEquals(recentSecretOther.id, list[0].first.id)
|
||||
assertEquals(recentSecretMine.id, list[1].first.id)
|
||||
assertEquals(recentPublic.id, list[2].first.id)
|
||||
assertEquals(3, totalCount)
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ class ChannelDonationMessageRepositoryTest @Autowired constructor(
|
||||
fun shouldFetchMemberWithDonationMessages() {
|
||||
// given: 크리에이터/후원자와 조회 대상 메시지를 준비한다.
|
||||
val creator = saveMember(nickname = "creator3", role = MemberRole.CREATOR)
|
||||
val viewer = saveMember(nickname = "viewer3", role = MemberRole.USER)
|
||||
val viewer = saveMember(nickname = "deleted_viewer3", role = MemberRole.USER)
|
||||
val message = saveMessage(member = viewer, creator = creator, can = 1, isSecret = false)
|
||||
val monthStart = LocalDateTime.now().withDayOfMonth(1).toLocalDate().atStartOfDay()
|
||||
val nextMonthStart = monthStart.plusMonths(1)
|
||||
@@ -160,8 +160,9 @@ class ChannelDonationMessageRepositoryTest @Autowired constructor(
|
||||
|
||||
// then: 조회 결과의 후원자 정보는 세션 없이도 접근 가능해야 한다.
|
||||
assertEquals(1, list.size)
|
||||
assertEquals(viewer.id, list[0].member!!.id)
|
||||
assertEquals("viewer3", list[0].member!!.nickname)
|
||||
assertEquals(viewer.id, list[0].first.member!!.id)
|
||||
assertEquals("deleted_viewer3", list[0].first.member!!.nickname)
|
||||
assertEquals("viewer3", list[0].second)
|
||||
}
|
||||
|
||||
private fun saveMember(nickname: String, role: MemberRole): Member {
|
||||
|
||||
+16
-9
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.explorer.profile.channelDonation
|
||||
|
||||
import kr.co.vividnext.sodalive.can.payment.CanPaymentService
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
@@ -97,9 +98,10 @@ class ChannelDonationServiceTest {
|
||||
Mockito.eq(0L),
|
||||
Mockito.eq(5L),
|
||||
anyLocalDateTime(),
|
||||
anyLocalDateTime()
|
||||
anyLocalDateTime(),
|
||||
Mockito.eq(Lang.KO) ?: Lang.KO
|
||||
)
|
||||
).thenReturn(listOf(message))
|
||||
).thenReturn(listOf(message to viewer.nickname))
|
||||
|
||||
// when: 채널 후원 목록 조회를 실행한다.
|
||||
val result = service.getChannelDonationList(
|
||||
@@ -129,7 +131,8 @@ class ChannelDonationServiceTest {
|
||||
Mockito.eq(0L),
|
||||
Mockito.eq(5L),
|
||||
anyLocalDateTime(),
|
||||
anyLocalDateTime()
|
||||
anyLocalDateTime(),
|
||||
Mockito.eq(Lang.KO) ?: Lang.KO
|
||||
)
|
||||
|
||||
// then: 월 시작/다음 달 시작 범위를 사용해야 한다.
|
||||
@@ -181,9 +184,10 @@ class ChannelDonationServiceTest {
|
||||
Mockito.eq(0L),
|
||||
Mockito.eq(5L),
|
||||
anyLocalDateTime(),
|
||||
anyLocalDateTime()
|
||||
anyLocalDateTime(),
|
||||
Mockito.eq(Lang.KO) ?: Lang.KO
|
||||
)
|
||||
).thenReturn(listOf(message))
|
||||
).thenReturn(listOf(message to "withdrawn"))
|
||||
|
||||
// when: 채널 후원 목록 조회를 실행한다.
|
||||
val result = service.getChannelDonationList(
|
||||
@@ -228,9 +232,10 @@ class ChannelDonationServiceTest {
|
||||
Mockito.eq(0L),
|
||||
Mockito.eq(5L),
|
||||
anyLocalDateTime(),
|
||||
anyLocalDateTime()
|
||||
anyLocalDateTime(),
|
||||
Mockito.eq(Lang.KO) ?: Lang.KO
|
||||
)
|
||||
).thenReturn(listOf(message))
|
||||
).thenReturn(listOf(message to viewer.nickname))
|
||||
|
||||
// when: 목록 조회를 실행한다.
|
||||
val result = service.getChannelDonationList(
|
||||
@@ -275,7 +280,8 @@ class ChannelDonationServiceTest {
|
||||
Mockito.eq(0L),
|
||||
Mockito.eq(5L),
|
||||
anyLocalDateTime(),
|
||||
anyLocalDateTime()
|
||||
anyLocalDateTime(),
|
||||
Mockito.eq(Lang.KO) ?: Lang.KO
|
||||
)
|
||||
).thenReturn(emptyList())
|
||||
|
||||
@@ -302,7 +308,8 @@ class ChannelDonationServiceTest {
|
||||
Mockito.eq(0L),
|
||||
Mockito.eq(5L),
|
||||
anyLocalDateTime(),
|
||||
anyLocalDateTime()
|
||||
anyLocalDateTime(),
|
||||
Mockito.eq(Lang.KO) ?: Lang.KO
|
||||
)
|
||||
|
||||
// then: 월 시작/다음 달 시작 범위를 사용해야 한다.
|
||||
|
||||
+1
@@ -150,6 +150,7 @@ class CreatorCommunityServiceTest {
|
||||
assertEquals(FcmEventType.INDIVIDUAL, event.type)
|
||||
assertEquals(PushNotificationCategory.COMMUNITY, event.category)
|
||||
assertEquals("creator.community.fcm.new_comment", event.messageKey)
|
||||
assertEquals(commenter.id, event.titleMemberId)
|
||||
assertEquals(commenter.id, event.senderMemberId)
|
||||
assertEquals(listOf(creator.id!!), event.recipients)
|
||||
assertEquals(FcmDeepLinkValue.COMMUNITY, event.deepLinkValue)
|
||||
|
||||
+2
-1
@@ -94,7 +94,8 @@ class LiveRecommendRepositoryTest @Autowired constructor(
|
||||
memberId = viewer.id!!,
|
||||
offset = 0,
|
||||
limit = 20,
|
||||
isBlocked = { false }
|
||||
isBlocked = { false },
|
||||
lang = Lang.KO
|
||||
)
|
||||
|
||||
assertEquals(2, result.size)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.live.recommend
|
||||
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.auth.Auth
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
@@ -28,7 +29,8 @@ class LiveRecommendServiceTest {
|
||||
repository = repository,
|
||||
blockMemberRepository = blockMemberRepository,
|
||||
memberContentPreferenceService = memberContentPreferenceService,
|
||||
liveRecommendCacheService = liveRecommendCacheService
|
||||
liveRecommendCacheService = liveRecommendCacheService,
|
||||
langContext = LangContext()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+523
@@ -0,0 +1,523 @@
|
||||
package kr.co.vividnext.sodalive.live.room
|
||||
|
||||
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
|
||||
import kr.co.vividnext.sodalive.can.use.CanUsage
|
||||
import kr.co.vividnext.sodalive.can.use.UseCan
|
||||
import kr.co.vividnext.sodalive.can.use.UseCanRepository
|
||||
import kr.co.vividnext.sodalive.fcm.FcmService
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.live.recommend.LiveRecommendService
|
||||
import kr.co.vividnext.sodalive.live.reservation.LiveReservation
|
||||
import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
|
||||
import kr.co.vividnext.sodalive.live.reservation.LiveReservationService
|
||||
import kr.co.vividnext.sodalive.live.reservation.MakeLiveReservationRequest
|
||||
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfo
|
||||
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository
|
||||
import kr.co.vividnext.sodalive.live.room.info.LiveRoomMemberRole
|
||||
import kr.co.vividnext.sodalive.live.room.visit.LiveRoomVisitService
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
|
||||
import kr.co.vividnext.sodalive.member.following.CreatorFollowingRepository
|
||||
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||
import org.hibernate.SessionFactory
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertAll
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.mock.mockito.MockBean
|
||||
import org.springframework.cache.CacheManager
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.jdbc.core.JdbcTemplate
|
||||
import org.springframework.mock.web.MockHttpServletRequest
|
||||
import org.springframework.test.annotation.DirtiesContext
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import org.springframework.transaction.annotation.Propagation
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.web.context.request.RequestContextHolder
|
||||
import org.springframework.web.context.request.ServletRequestAttributes
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.EntityManagerFactory
|
||||
|
||||
@SpringBootTest(
|
||||
properties = [
|
||||
"spring.cache.type=none",
|
||||
"spring.jpa.properties.hibernate.generate_statistics=true",
|
||||
"jwt.secret=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWYwMTIzNDU2Nzg5YWJjZGVmMDEyMzQ1Njc4OWFiY2RlZg==",
|
||||
"agora.app-id=0123456789abcdef0123456789abcdef",
|
||||
"agora.app-certificate=0123456789abcdef0123456789abcdef"
|
||||
]
|
||||
)
|
||||
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
|
||||
@Transactional
|
||||
class LiveRoomLocalizedNameIntegrationTest @Autowired constructor(
|
||||
private val service: LiveRoomService,
|
||||
private val reservationService: LiveReservationService,
|
||||
private val reservationRepository: LiveReservationRepository,
|
||||
private val visitService: LiveRoomVisitService,
|
||||
private val recommendService: LiveRecommendService,
|
||||
private val followingRepository: CreatorFollowingRepository,
|
||||
private val useCanRepository: UseCanRepository,
|
||||
private val roomRepository: LiveRoomRepository,
|
||||
private val roomInfoRepository: LiveRoomInfoRedisRepository,
|
||||
private val memberRepository: MemberRepository,
|
||||
private val jdbcTemplate: JdbcTemplate,
|
||||
private val cacheManager: CacheManager,
|
||||
private val entityManagerFactory: EntityManagerFactory,
|
||||
private val langContext: LangContext
|
||||
) {
|
||||
@MockBean
|
||||
private lateinit var fcmService: FcmService
|
||||
|
||||
@MockBean
|
||||
private lateinit var s3Uploader: S3Uploader
|
||||
|
||||
private val roomIds = mutableListOf<Long>()
|
||||
private val finishedLiveCacheKeys = mutableListOf<String>()
|
||||
|
||||
@BeforeEach
|
||||
fun setUpRequest() {
|
||||
RequestContextHolder.setRequestAttributes(ServletRequestAttributes(MockHttpServletRequest()))
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun resetRequestAndRemoveRoomInfo() {
|
||||
roomIds.forEach { roomInfoRepository.deleteById(it) }
|
||||
finishedLiveCacheKeys.forEach { cacheManager.getCache("cache_ttl_10_minutes")?.evict(it) }
|
||||
RequestContextHolder.resetRequestAttributes()
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("라이브 info는 모든 멤버 목록을 요청 언어로 복사하고 역할과 공유 Redis 원본을 보존한다")
|
||||
fun shouldRenderRoomInfoMemberNamesWithoutMutatingSharedRedisMembership() {
|
||||
val fixture = createFixture()
|
||||
langContext.setLang(Lang.JA)
|
||||
val japanese = service.getRoomInfo(fixture.roomId, fixture.viewer)
|
||||
langContext.setLang(Lang.KO)
|
||||
val korean = service.getRoomInfo(fixture.roomId, fixture.viewer)
|
||||
val stored = roomInfoRepository.findById(fixture.roomId).orElseThrow()
|
||||
|
||||
assertAll(
|
||||
{ assertEquals("日本-host", japanese.creatorNickname) },
|
||||
{ assertEquals("日本-host", japanese.speakerList.single().nickname) },
|
||||
{ assertEquals("日本-manager", japanese.managerList.single().nickname) },
|
||||
{ assertEquals(listOf("viewer", "fallback"), japanese.listenerList.map { it.nickname }) },
|
||||
{ assertEquals(LiveRoomMemberRole.SPEAKER, japanese.speakerList.single().role) },
|
||||
{ assertEquals(LiveRoomMemberRole.MANAGER, japanese.managerList.single().role) },
|
||||
{ assertEquals("host", korean.creatorNickname) },
|
||||
{ assertEquals("host", korean.speakerList.single().nickname) },
|
||||
{ assertEquals("manager", korean.managerList.single().nickname) },
|
||||
{ assertEquals("host", stored.speakerList.single().nickname) },
|
||||
{ assertEquals("manager", stored.managerList.single().nickname) },
|
||||
{ assertEquals("host", memberRepository.findById(fixture.host.id!!).orElseThrow().nickname) }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("라이브 상세와 멤버 프로필의 현재 이름은 JA 별칭이며 USER와 비활성 별칭은 원본이다")
|
||||
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
|
||||
fun shouldRenderCurrentRoomDetailAndMemberProfileNames() {
|
||||
val fixture = createFixture()
|
||||
langContext.setLang(Lang.JA)
|
||||
|
||||
val detail = service.getRoomDetail(fixture.roomId, fixture.viewer, "UTC")
|
||||
val hostProfile = service.getUserProfile(fixture.roomId, fixture.host.id!!, fixture.viewer)
|
||||
val viewerProfile = service.getUserProfile(fixture.roomId, fixture.viewer.id!!, fixture.viewer)
|
||||
|
||||
assertAll(
|
||||
{ assertEquals("日本-host", detail.manager!!.nickname) },
|
||||
{ assertEquals("日本-host", detail.participatingUsers.single { it.id == fixture.host.id }.nickname) },
|
||||
{ assertEquals("日本-manager", detail.participatingUsers.single { it.id == fixture.manager.id }.nickname) },
|
||||
{ assertEquals("viewer", detail.participatingUsers.single { it.id == fixture.viewer.id }.nickname) },
|
||||
{ assertEquals("fallback", detail.participatingUsers.single { it.id == fixture.fallback.id }.nickname) },
|
||||
{ assertEquals("日本-host", hostProfile.nickname) },
|
||||
{ assertEquals("viewer", viewerProfile.nickname) }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("현재 후원 룰렛 메시지 이름은 요청별로 렌더링하며 비밀 필터와 본문 UUID 및 Redis 원본은 보존한다")
|
||||
fun shouldRenderDonationAndRouletteNamesOnCopiesAfterSecretFiltering() {
|
||||
val fixture = createFixture()
|
||||
val roomInfo = roomInfoRepository.findById(fixture.roomId).orElseThrow()
|
||||
roomInfo.addDonationMessage(fixture.manager.id!!, "manager", false, 10, "manager 후원 본문")
|
||||
roomInfo.addRouletteMessage(fixture.manager.id!!, "manager", "manager 룰렛 결과")
|
||||
roomInfo.addDonationMessage(fixture.viewer.id!!, "viewer", false, 10, "viewer 본문")
|
||||
roomInfo.addDonationMessage(fixture.manager.id!!, "manager", true, 10, "비밀 본문")
|
||||
roomInfoRepository.save(roomInfo)
|
||||
langContext.setLang(Lang.JA)
|
||||
val japanese = service.getDonationMessageList(fixture.roomId, fixture.viewer)
|
||||
langContext.setLang(Lang.KO)
|
||||
val korean = service.getDonationMessageList(fixture.roomId, fixture.viewer)
|
||||
val stored = roomInfoRepository.findById(fixture.roomId).orElseThrow().donationMessageList
|
||||
|
||||
assertAll(
|
||||
{ assertEquals(listOf("日本-manager", "日本-manager", "viewer"), japanese.map { it.nickname }) },
|
||||
{ assertEquals(listOf("manager", "manager", "viewer"), korean.map { it.nickname }) },
|
||||
{ assertEquals(listOf("manager 후원 본문", "manager 룰렛 결과", "viewer 본문"), japanese.map { it.donationMessage }) },
|
||||
{ assertEquals(korean.map { it.uuid }, japanese.map { it.uuid }) },
|
||||
{ assertEquals(listOf("manager", "manager", "viewer", "manager"), stored.map { it.nickname }) },
|
||||
{ assertEquals(4, stored.size) }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("최근 종료 라이브의 현재 이름은 같은 회원의 KO 조회 캐시 이후에도 JA 요청을 따른다")
|
||||
fun shouldRenderJapaneseFinishedLiveNameAfterKoreanCacheRead() {
|
||||
val fixture = createFixture()
|
||||
val room = roomRepository.findById(fixture.roomId).orElseThrow()
|
||||
room.isActive = false
|
||||
fixture.host.profileImage = "profile/host.png"
|
||||
roomRepository.flush()
|
||||
val cacheKey = "getLatestFinishedLive:${fixture.viewer.id}"
|
||||
finishedLiveCacheKeys.add(cacheKey)
|
||||
val cache = requireNotNull(cacheManager.getCache("cache_ttl_10_minutes"))
|
||||
cache.evict(cacheKey)
|
||||
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
|
||||
statistics.clear()
|
||||
langContext.setLang(Lang.KO)
|
||||
val korean = service.getLatestFinishedLive(fixture.viewer).single { it.memberId == fixture.host.id }
|
||||
assertEquals(1L, currentNameQueryCount())
|
||||
val roomListQuery = statistics.queries.single { it.contains("from LiveRoom") }
|
||||
val roomListExecutions = statistics.getQueryStatistics(roomListQuery).executionCount
|
||||
val cached = cache.get(cacheKey)?.get()
|
||||
assertNotNull(cached)
|
||||
assertEquals(1L, roomListExecutions)
|
||||
assertEquals("host", (cached as List<*>).filterIsInstance<GetLatestFinishedLiveResponse>().single().nickname)
|
||||
langContext.setLang(Lang.JA)
|
||||
val nameQueryCountBeforeHit = currentNameQueryCount()
|
||||
val japanese = service.getLatestFinishedLive(fixture.viewer).single { it.memberId == fixture.host.id }
|
||||
assertEquals(1L, currentNameQueryCount() - nameQueryCountBeforeHit)
|
||||
assertEquals(roomListExecutions, statistics.getQueryStatistics(roomListQuery).executionCount)
|
||||
assertEquals(cached, cache.get(cacheKey)?.get())
|
||||
fixture.host.nickname = "updated-host"
|
||||
memberRepository.flush()
|
||||
jdbcTemplate.update("UPDATE creator_localized_name SET is_active = false WHERE member_id = ?", fixture.host.id!!)
|
||||
val nameQueryCountBeforeFallback = currentNameQueryCount()
|
||||
val fallback = service.getLatestFinishedLive(fixture.viewer).single { it.memberId == fixture.host.id }
|
||||
assertEquals(1L, currentNameQueryCount() - nameQueryCountBeforeFallback)
|
||||
|
||||
assertEquals("host", korean.nickname)
|
||||
assertEquals("日本-host", japanese.nickname)
|
||||
assertEquals("updated-host", fallback.nickname)
|
||||
assertEquals("host", korean.nickname)
|
||||
assertEquals(roomListExecutions, statistics.getQueryStatistics(roomListQuery).executionCount)
|
||||
assertEquals(cached, cache.get(cacheKey)?.get())
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
|
||||
"VALUES (?, 'EN', 'English host', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
||||
fixture.host.id!!
|
||||
)
|
||||
langContext.setLang(Lang.EN)
|
||||
val nameQueryCountBeforeEnglishHit = currentNameQueryCount()
|
||||
val english = service.getLatestFinishedLive(fixture.viewer).single { it.memberId == fixture.host.id }
|
||||
assertEquals("English host", english.nickname)
|
||||
assertEquals(1L, currentNameQueryCount() - nameQueryCountBeforeEnglishHit)
|
||||
assertEquals(roomListExecutions, statistics.getQueryStatistics(roomListQuery).executionCount)
|
||||
assertEquals(cached, cache.get(cacheKey)?.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("언어 중립 live feed와 종료 캐시는 별칭 해제 후 변경된 현재 원본으로 fallback한다")
|
||||
fun shouldFallBackToCurrentOriginalWithoutRewritingNeutralRedisNames() {
|
||||
val fixture = createFixture()
|
||||
val info = roomInfoRepository.findById(fixture.roomId).orElseThrow()
|
||||
info.addDonationMessage(fixture.host.id!!, "host", false, 10, "host 본문")
|
||||
roomInfoRepository.save(info)
|
||||
fixture.host.nickname = "updated-host"
|
||||
memberRepository.flush()
|
||||
jdbcTemplate.update("UPDATE creator_localized_name SET is_active = false WHERE member_id = ?", fixture.host.id!!)
|
||||
langContext.setLang(Lang.JA)
|
||||
|
||||
val response = service.getRoomInfo(fixture.roomId, fixture.viewer)
|
||||
val messages = service.getDonationMessageList(fixture.roomId, fixture.viewer)
|
||||
val stored = roomInfoRepository.findById(fixture.roomId).orElseThrow()
|
||||
|
||||
assertEquals("updated-host", response.creatorNickname)
|
||||
assertEquals("updated-host", response.speakerList.single().nickname)
|
||||
assertEquals("updated-host", messages.single().nickname)
|
||||
assertEquals("host", stored.speakerList.single().nickname)
|
||||
assertEquals("host", stored.donationMessageList.single().nickname)
|
||||
assertEquals("host 본문", messages.single().donationMessage)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("예약 생성·목록·상세와 최근 방문자 조회는 실제 크리에이터 소유 ID로 JA 이름을 표시한다")
|
||||
fun shouldLocalizeReservationAndRecentVisitorNames() {
|
||||
val fixture = createFixture()
|
||||
langContext.setLang(Lang.JA)
|
||||
val reservation = reservationService.makeReservation(
|
||||
MakeLiveReservationRequest(fixture.roomId, "web", "UTC"),
|
||||
fixture.viewer.id!!
|
||||
)
|
||||
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
|
||||
statistics.clear()
|
||||
val listed = reservationService.getReservationList(fixture.viewer.id!!, true, "UTC").single()
|
||||
val detail = reservationService.getReservation(reservation.reservationId, fixture.viewer.id!!, "UTC")
|
||||
assertEquals(2L, statistics.queryExecutionCount, statistics.queries.joinToString("\n"))
|
||||
val room = roomRepository.findById(fixture.roomId).orElseThrow()
|
||||
visitService.roomVisit(room, fixture.viewer)
|
||||
visitService.roomVisit(room, fixture.manager)
|
||||
statistics.clear()
|
||||
val visitor = visitService.getRecentVisitRoomUsers(fixture.viewer).single()
|
||||
assertEquals(2L, statistics.queryExecutionCount, statistics.queries.joinToString("\n"))
|
||||
|
||||
assertEquals("日本-host", reservation.nickname)
|
||||
assertEquals("日本-host", listed.masterNickname)
|
||||
assertEquals("日本-host", detail.masterNickname)
|
||||
assertEquals(fixture.manager.id, visitor.id)
|
||||
assertEquals("日本-manager", visitor.nickname)
|
||||
langContext.setLang(Lang.KO)
|
||||
assertEquals("manager", visitService.getRecentVisitRoomUsers(fixture.viewer).single().nickname)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("추천과 팔로잉의 on-air 및 off-air 분기는 요청별 이름을 표시하고 count와 순서를 보존한다")
|
||||
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
|
||||
fun shouldLocalizeRecommendedAndFollowingChannels() {
|
||||
val fixture = createFixture()
|
||||
followingRepository.saveAndFlush(
|
||||
CreatorFollowing().apply {
|
||||
member = fixture.viewer
|
||||
creator = fixture.host
|
||||
}
|
||||
)
|
||||
followingRepository.saveAndFlush(
|
||||
CreatorFollowing().apply {
|
||||
member = fixture.viewer
|
||||
creator = fixture.manager
|
||||
}
|
||||
)
|
||||
langContext.setLang(Lang.JA)
|
||||
|
||||
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
|
||||
statistics.clear()
|
||||
val recommended = recommendService.getRecommendChannelList(null).associateBy { it.creatorId }
|
||||
val aliasQueries = statistics.queries.filter {
|
||||
Regex("""(?is)^select\s+\w+\.memberId,\s*\w+\.displayName\s+from\s+CreatorLocalizedNameEntity\b""")
|
||||
.containsMatchIn(it)
|
||||
}
|
||||
val aliasQueryCount = aliasQueries.sumOf { statistics.getQueryStatistics(it).executionCount }
|
||||
val queryCount = statistics.queryExecutionCount
|
||||
val following = recommendService.getFollowingChannelList(fixture.viewer).associateBy { it.creatorId }
|
||||
val all = recommendService.getFollowingAllChannelList(fixture.viewer, PageRequest.of(0, 20))
|
||||
|
||||
assertEquals("日本-host", recommended.getValue(fixture.host.id!!).nickname)
|
||||
assertEquals("日本-manager", recommended.getValue(fixture.manager.id!!).nickname)
|
||||
assertEquals("fallback", recommended.getValue(fixture.fallback.id!!).nickname)
|
||||
assertEquals("日本-host", following.getValue(fixture.host.id!!).nickname)
|
||||
assertEquals("日本-manager", following.getValue(fixture.manager.id!!).nickname)
|
||||
assertEquals(2, all.totalCount)
|
||||
assertEquals(setOf("日本-host", "日本-manager"), all.items.map { it.nickname }.toSet())
|
||||
langContext.setLang(Lang.KO)
|
||||
assertEquals(
|
||||
setOf("host", "manager"),
|
||||
recommendService.getFollowingAllChannelList(fixture.viewer, PageRequest.of(0, 20)).items.map { it.nickname }.toSet()
|
||||
)
|
||||
assertEquals(0L, aliasQueryCount, "Standalone alias SELECTs (total queries=$queryCount): $aliasQueries")
|
||||
assertNoStandaloneNameQueries()
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
|
||||
"VALUES (?, 'EN', 'English host', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
||||
fixture.host.id!!
|
||||
)
|
||||
langContext.setLang(Lang.EN)
|
||||
assertEquals(
|
||||
setOf("English host", "manager"),
|
||||
recommendService.getFollowingAllChannelList(fixture.viewer, PageRequest.of(0, 20)).items.map { it.nickname }.toSet()
|
||||
)
|
||||
assertNoStandaloneNameQueries()
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("후원과 하트 집계는 donor ID로 이름만 바꾸고 USER·금액·비밀 합계와 공개 DTO 필드를 보존한다")
|
||||
fun shouldLocalizeDonationAndHeartDonorNamesWithoutChangingAmounts() {
|
||||
val fixture = createFixture()
|
||||
val room = roomRepository.findById(fixture.roomId).orElseThrow()
|
||||
listOf(CanUsage.DONATION, CanUsage.HEART).forEach { usage ->
|
||||
useCanRepository.saveAndFlush(
|
||||
UseCan(usage, 10, 2).apply {
|
||||
member = fixture.manager
|
||||
this.room = room
|
||||
}
|
||||
)
|
||||
useCanRepository.saveAndFlush(
|
||||
UseCan(usage, 5, 1).apply {
|
||||
member = fixture.viewer
|
||||
this.room = room
|
||||
}
|
||||
)
|
||||
}
|
||||
langContext.setLang(Lang.JA)
|
||||
|
||||
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
|
||||
statistics.clear()
|
||||
val donations = service.getDonationStatus(fixture.roomId, fixture.host.id!!)
|
||||
val hearts = service.getHeartList(fixture.roomId)
|
||||
assertEquals(3L, statistics.queryExecutionCount, statistics.queries.joinToString("\n"))
|
||||
|
||||
assertEquals(listOf("日本-manager", "viewer"), donations.donationList.map { it.nickname })
|
||||
assertEquals(listOf("日本-manager", "viewer"), hearts.heartList.map { it.nickname })
|
||||
assertEquals(18, donations.totalCan)
|
||||
assertEquals(0, donations.totalSecretCan)
|
||||
assertEquals(18, hearts.totalHeart)
|
||||
assertEquals(2, hearts.totalCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("진행·날짜별 예약·예정 라이브 목록은 EN 이름을 조인하고 USER·미등록·비활성 소유자를 유지한다")
|
||||
fun shouldJoinEnglishNamesInEveryDatabaseRoomListWithoutMutatingOwners() {
|
||||
val fixture = createFixture()
|
||||
val owners = listOf(fixture.host, fixture.manager, fixture.viewer, fixture.fallback)
|
||||
listOf(fixture.host, fixture.viewer, fixture.fallback).forEach {
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
|
||||
"VALUES (?, 'EN', ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
||||
it.id!!,
|
||||
"EN-${it.nickname}",
|
||||
it != fixture.fallback
|
||||
)
|
||||
}
|
||||
val rooms = listOf(roomRepository.findById(fixture.roomId).orElseThrow()) + owners.drop(1).map { owner ->
|
||||
roomRepository.saveAndFlush(
|
||||
LiveRoom("${owner.nickname} room", "notice", LocalDateTime.now(), 10, "cover.png", isAdult = false).apply {
|
||||
member = owner
|
||||
channelName = "live_${owner.id}"
|
||||
}
|
||||
)
|
||||
}
|
||||
val expected = mapOf(
|
||||
fixture.host.id to "EN-host",
|
||||
fixture.manager.id to "manager",
|
||||
fixture.viewer.id to "viewer",
|
||||
fixture.fallback.id to "fallback"
|
||||
)
|
||||
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
|
||||
statistics.clear()
|
||||
|
||||
val current = roomRepository.getLiveRoomListNow(0, 20, "UTC", null, false, true, null, Lang.EN)
|
||||
assertEquals(expected, current.associate { it.room.member!!.id to it.creatorNickname })
|
||||
assertEquals(1L, statistics.queryExecutionCount)
|
||||
|
||||
owners.forEach { owner ->
|
||||
reservationRepository.saveAndFlush(
|
||||
LiveReservation().apply {
|
||||
member = owner
|
||||
room = rooms.first()
|
||||
}
|
||||
)
|
||||
}
|
||||
statistics.clear()
|
||||
val bookers = reservationRepository.getReservationList(fixture.roomId, Lang.EN)
|
||||
assertEquals(expected, bookers.associate { it.id to it.nickname })
|
||||
assertEquals(1L, statistics.queryExecutionCount)
|
||||
statistics.clear()
|
||||
val reservation = reservationRepository.getReservationListByMemberId(fixture.viewer.id!!, true, Lang.EN).single()
|
||||
val reserved = reservationRepository.getReservationByReservationAndMemberId(reservation.id, fixture.viewer.id!!, Lang.EN)
|
||||
assertEquals("EN-host", reservation.masterNickname)
|
||||
assertEquals("EN-host", reserved!!.masterNickname)
|
||||
assertEquals(2L, statistics.queryExecutionCount)
|
||||
|
||||
val date = LocalDateTime.now().plusDays(1).withHour(12).withMinute(0)
|
||||
rooms.forEach {
|
||||
it.channelName = null
|
||||
it.beginDateTime = date
|
||||
}
|
||||
roomRepository.flush()
|
||||
statistics.clear()
|
||||
val dated = roomRepository.getLiveRoomListReservationWithDate(
|
||||
date.toLocalDate().atStartOfDay(),
|
||||
0,
|
||||
20,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
Lang.EN
|
||||
)
|
||||
val upcoming = roomRepository.getLiveRoomListReservationWithoutDate("UTC", null, false, true, null, Lang.EN)
|
||||
|
||||
assertEquals(expected, dated.associate { it.room.member!!.id to it.creatorNickname })
|
||||
assertEquals(expected, upcoming.associate { it.room.member!!.id to it.creatorNickname })
|
||||
assertEquals(2L, statistics.queryExecutionCount)
|
||||
assertEquals(listOf("host", "manager", "viewer", "fallback"), owners.map { it.nickname })
|
||||
assertNoStandaloneNameQueries()
|
||||
}
|
||||
|
||||
private fun assertNoStandaloneNameQueries() {
|
||||
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
|
||||
val nameQueries = statistics.queries.filter {
|
||||
Regex("""(?is)\bfrom\s+CreatorLocalizedNameEntity\b""").containsMatchIn(it) ||
|
||||
Regex("""(?is)^select\s+\w+\.id,\s*coalesce\([^)]*\)\s+from\s+Member\b""").containsMatchIn(it)
|
||||
}
|
||||
assertEquals(0L, nameQueries.sumOf { statistics.getQueryStatistics(it).executionCount }, nameQueries.joinToString("\n"))
|
||||
}
|
||||
|
||||
private fun currentNameQueryCount(): Long {
|
||||
val statistics = entityManagerFactory.unwrap(SessionFactory::class.java).statistics
|
||||
return statistics.queries.filter { it.contains("from Member") && it.contains("CreatorLocalizedNameEntity") }
|
||||
.sumOf { statistics.getQueryStatistics(it).executionCount }
|
||||
}
|
||||
|
||||
private fun createFixture(): Fixture {
|
||||
val host = savedMember("host", MemberRole.CREATOR)
|
||||
val manager = savedMember("manager", MemberRole.CREATOR)
|
||||
val viewer = savedMember("viewer", MemberRole.USER)
|
||||
val fallback = savedMember("fallback", MemberRole.CREATOR, activeAlias = false)
|
||||
val room = roomRepository.saveAndFlush(
|
||||
LiveRoom(
|
||||
title = "room title",
|
||||
notice = "notice",
|
||||
beginDateTime = LocalDateTime.now().minusMinutes(1),
|
||||
numberOfPeople = 10,
|
||||
coverImage = "cover.png",
|
||||
isAdult = false
|
||||
).apply {
|
||||
member = host
|
||||
channelName = "localized_live_${host.id}"
|
||||
}
|
||||
)
|
||||
roomIds.add(room.id!!)
|
||||
roomInfoRepository.save(
|
||||
LiveRoomInfo(room.id!!).apply {
|
||||
addSpeaker(host, "https://cdn.test")
|
||||
addManager(manager, "https://cdn.test")
|
||||
addListener(viewer, "https://cdn.test")
|
||||
addListener(fallback, "https://cdn.test")
|
||||
}
|
||||
)
|
||||
return Fixture(room.id!!, host, manager, viewer, fallback)
|
||||
}
|
||||
|
||||
private fun savedMember(nickname: String, role: MemberRole, activeAlias: Boolean = true): Member {
|
||||
val member = memberRepository.saveAndFlush(
|
||||
Member(
|
||||
email = "$nickname@localized-live.test",
|
||||
password = "pw",
|
||||
nickname = nickname,
|
||||
profileImage = "profile/$nickname.png",
|
||||
role = role,
|
||||
isVisibleDonationRank = false
|
||||
)
|
||||
)
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
|
||||
"VALUES (?, 'JA', ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
||||
member.id!!,
|
||||
"日本-$nickname",
|
||||
activeAlias
|
||||
)
|
||||
return member
|
||||
}
|
||||
|
||||
private data class Fixture(val roomId: Long, val host: Member, val manager: Member, val viewer: Member, val fallback: Member)
|
||||
}
|
||||
+15
-6
@@ -12,6 +12,7 @@ import kr.co.vividnext.sodalive.content.ContentType
|
||||
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
|
||||
import kr.co.vividnext.sodalive.explorer.profile.CreatorDonationRankingService
|
||||
import kr.co.vividnext.sodalive.fcm.PushTokenRepository
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||
import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
|
||||
@@ -30,6 +31,7 @@ import kr.co.vividnext.sodalive.member.MemberRepository
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.ViewerContentPreference
|
||||
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
@@ -106,6 +108,7 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
|
||||
menuService = menuService,
|
||||
messageSource = messageSource,
|
||||
langContext = langContext,
|
||||
localizedNameService = mock<CreatorLocalizedNameService>(),
|
||||
repository = repository,
|
||||
rouletteRepository = rouletteRepository,
|
||||
roomInfoRepository = roomInfoRepository,
|
||||
@@ -152,7 +155,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
|
||||
memberId = 100L,
|
||||
isCreator = false,
|
||||
isAdult = true,
|
||||
effectiveGender = Gender.NONE
|
||||
effectiveGender = Gender.NONE,
|
||||
lang = Lang.KO
|
||||
)
|
||||
).thenReturn(emptyList())
|
||||
|
||||
@@ -172,7 +176,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
|
||||
memberId = 100L,
|
||||
isCreator = false,
|
||||
isAdult = true,
|
||||
effectiveGender = Gender.NONE
|
||||
effectiveGender = Gender.NONE,
|
||||
lang = Lang.KO
|
||||
)
|
||||
}
|
||||
|
||||
@@ -187,7 +192,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
|
||||
memberId = null,
|
||||
isCreator = false,
|
||||
isAdult = true,
|
||||
effectiveGender = null
|
||||
effectiveGender = null,
|
||||
lang = Lang.KO
|
||||
)
|
||||
).thenReturn(emptyList())
|
||||
|
||||
@@ -207,7 +213,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
|
||||
memberId = null,
|
||||
isCreator = false,
|
||||
isAdult = true,
|
||||
effectiveGender = null
|
||||
effectiveGender = null,
|
||||
lang = Lang.KO
|
||||
)
|
||||
}
|
||||
|
||||
@@ -222,7 +229,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
|
||||
memberId = 200L,
|
||||
isCreator = false,
|
||||
isAdult = false,
|
||||
effectiveGender = Gender.NONE
|
||||
effectiveGender = Gender.NONE,
|
||||
lang = Lang.KO
|
||||
)
|
||||
).thenReturn(emptyList())
|
||||
|
||||
@@ -240,7 +248,8 @@ class LiveRoomServiceAdultVisibilityPolicyTest {
|
||||
memberId = 200L,
|
||||
isCreator = false,
|
||||
isAdult = false,
|
||||
effectiveGender = Gender.NONE
|
||||
effectiveGender = Gender.NONE,
|
||||
lang = Lang.KO
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.auth.Auth
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
@@ -70,6 +71,7 @@ class LiveRoomServiceTest {
|
||||
menuService = mock<LiveRoomMenuService>(),
|
||||
messageSource = mock<SodaMessageSource>(),
|
||||
langContext = LangContext(),
|
||||
localizedNameService = mock<CreatorLocalizedNameService>(),
|
||||
repository = repository,
|
||||
rouletteRepository = rouletteRepository,
|
||||
roomInfoRepository = roomInfoRepository,
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package kr.co.vividnext.sodalive.member
|
||||
|
||||
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||
import org.junit.jupiter.api.Assertions.assertAll
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.jdbc.core.JdbcTemplate
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@DataJpaTest(properties = ["spring.cache.type=none"])
|
||||
@Import(QueryDslConfig::class)
|
||||
class MemberRepositoryLocalizedNameTest @Autowired constructor(
|
||||
private val repository: MemberRepository,
|
||||
private val entityManager: EntityManager,
|
||||
private val jdbcTemplate: JdbcTemplate
|
||||
) {
|
||||
@Test
|
||||
@DisplayName("크리에이터 요청의 회원 검색은 USER 원본 매칭과 CREATOR 별칭을 함께 유지한다")
|
||||
fun shouldRetainUserOriginalMatchesAndAddOnlyCreatorAliasesForCreatorRequester() {
|
||||
val requester = saveMember("needle-requester", MemberRole.CREATOR)
|
||||
val creator = saveMember("creator-original", MemberRole.CREATOR)
|
||||
val user = saveMember("needle-user", MemberRole.USER)
|
||||
val aiUser = saveMember("ai-user-original", MemberRole.USER).apply { memberKind = MemberKind.AI_CHARACTER }
|
||||
val admin = saveMember("needle-admin", MemberRole.ADMIN)
|
||||
val agent = saveMember("needle-agent", MemberRole.AGENT)
|
||||
val bot = saveMember("needle-bot", MemberRole.BOT)
|
||||
listOf(requester, creator, user, aiUser, admin, agent, bot).forEach { saveAlias(it, "needle-alias") }
|
||||
entityManager.clear()
|
||||
|
||||
val matches = repository.findByNicknameAndOtherCondition("NeEdLe", requester)
|
||||
|
||||
assertEquals(setOf(creator.id, user.id), matches.map { it.id }.toSet())
|
||||
assertEquals(2, matches.size)
|
||||
assertEquals(listOf(creator.id), repository.findByNicknameAndOtherCondition("creator-original", requester).map { it.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("USER 요청의 회원 검색은 기존 USER·self·관리 역할 제외 조건을 별칭에도 적용한다")
|
||||
fun shouldPreserveUserRequesterRoleAndSelfExclusionsForAliases() {
|
||||
val requester = saveMember("needle-viewer", MemberRole.USER)
|
||||
val creator = saveMember("creator-original", MemberRole.CREATOR)
|
||||
val user = saveMember("needle-user", MemberRole.USER)
|
||||
val admin = saveMember("needle-admin", MemberRole.ADMIN)
|
||||
val agent = saveMember("needle-agent", MemberRole.AGENT)
|
||||
val bot = saveMember("needle-bot", MemberRole.BOT)
|
||||
listOf(requester, creator, user, admin, agent, bot).forEach { saveAlias(it, "needle-alias") }
|
||||
entityManager.clear()
|
||||
|
||||
assertEquals(listOf(creator.id), repository.findByNicknameAndOtherCondition("needle", requester).map { it.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("비활성 JA와 다른 언어 이름은 회원 별칭 검색에서 제외하고 현재 원본은 유지한다")
|
||||
fun shouldExcludeInactiveAndNonJaAliasesWhileRetainingCurrentOriginalNickname() {
|
||||
val requester = saveMember("member-clear-viewer", MemberRole.CREATOR)
|
||||
val creator = saveMember("before-rename", MemberRole.CREATOR)
|
||||
saveAlias(creator, "RetainedAlias", isActive = false)
|
||||
saveAlias(creator, "EnglishAlias", lang = "EN")
|
||||
creator.nickname = "after-rename"
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
assertEquals(emptyList<Long>(), repository.findByNicknameAndOtherCondition("RetainedAlias", requester).map { it.id })
|
||||
assertEquals(emptyList<Long>(), repository.findByNicknameAndOtherCondition("EnglishAlias", requester).map { it.id })
|
||||
assertEquals(listOf(creator.id), repository.findByNicknameAndOtherCondition("after-rename", requester).map { it.id })
|
||||
jdbcTemplate.update(
|
||||
"update creator_localized_name set is_active = true where member_id = ? and lang = 'JA'",
|
||||
creator.id!!
|
||||
)
|
||||
|
||||
assertAll(
|
||||
{
|
||||
assertEquals(
|
||||
listOf(creator.id),
|
||||
repository.findByNicknameAndOtherCondition("RetainedAlias", requester).map { it.id }
|
||||
)
|
||||
},
|
||||
{
|
||||
assertEquals(
|
||||
listOf(creator.id),
|
||||
repository.findByNicknameAndOtherCondition("after-rename", requester).map { it.id }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveMember(nickname: String, role: MemberRole): Member {
|
||||
return Member(email = "$nickname@test.com", password = "password", nickname = nickname, role = role).also {
|
||||
entityManager.persist(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAlias(member: Member, displayName: String, isActive: Boolean = true, lang: String = "JA") {
|
||||
entityManager.flush()
|
||||
jdbcTemplate.update(
|
||||
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
|
||||
"values (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
||||
member.id!!,
|
||||
lang,
|
||||
displayName,
|
||||
isActive
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.member.block.BlockMember
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
import kr.co.vividnext.sodalive.member.block.MemberBlockRequest
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.support.originalLocalizedNameService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -62,6 +63,7 @@ class MemberServiceCacheEvictionTest {
|
||||
authenticationManagerBuilder = mock(),
|
||||
messageSource = SodaMessageSource(),
|
||||
langContext = LangContext(),
|
||||
localizedNameService = originalLocalizedNameService(),
|
||||
countryContext = CountryContext(),
|
||||
memberContentPreferenceService = mock<MemberContentPreferenceService>(),
|
||||
objectMapper = ObjectMapper(),
|
||||
|
||||
@@ -13,6 +13,7 @@ import kr.co.vividnext.sodalive.member.stipulation.Stipulation
|
||||
import kr.co.vividnext.sodalive.member.stipulation.StipulationAgreeRepository
|
||||
import kr.co.vividnext.sodalive.member.stipulation.StipulationIds
|
||||
import kr.co.vividnext.sodalive.member.stipulation.StipulationRepository
|
||||
import kr.co.vividnext.sodalive.support.originalLocalizedNameService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
@@ -71,6 +72,7 @@ class MemberServiceContentPreferenceTest {
|
||||
authenticationManagerBuilder = mock(),
|
||||
messageSource = SodaMessageSource(),
|
||||
langContext = LangContext(),
|
||||
localizedNameService = originalLocalizedNameService(),
|
||||
countryContext = CountryContext(),
|
||||
memberContentPreferenceService = memberContentPreferenceService,
|
||||
objectMapper = ObjectMapper(),
|
||||
|
||||
@@ -250,7 +250,7 @@ class OsivLazyLoadingRegressionTest @Autowired constructor(
|
||||
|
||||
val creators = rankingRepository.getCreatorRankings(memberId = null)
|
||||
|
||||
val response = creators.single().toExplorerSectionCreator("https://cdn.test")
|
||||
val response = creators.single()
|
||||
assertEquals("#creator-tag", response.tags)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
package kr.co.vividnext.sodalive.search
|
||||
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.admin.content.series.genre.SeriesGenre
|
||||
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||
import kr.co.vividnext.sodalive.content.AudioContent
|
||||
import kr.co.vividnext.sodalive.content.ContentType
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.Series
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesContent
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMember
|
||||
import kr.co.vividnext.sodalive.member.tag.CreatorTag
|
||||
import kr.co.vividnext.sodalive.member.tag.MemberCreatorTag
|
||||
import org.hibernate.SessionFactory
|
||||
import org.junit.jupiter.api.Assertions.assertAll
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.jdbc.core.JdbcTemplate
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@DataJpaTest(properties = ["spring.cache.type=none", "spring.jpa.properties.hibernate.generate_statistics=true"])
|
||||
@Import(QueryDslConfig::class)
|
||||
class SearchRepositoryLocalizedNameTest @Autowired constructor(
|
||||
private val entityManager: EntityManager,
|
||||
private val jdbcTemplate: JdbcTemplate,
|
||||
queryFactory: JPAQueryFactory
|
||||
) {
|
||||
private val repository = SearchRepository(queryFactory, "https://cdn.test")
|
||||
|
||||
@Test
|
||||
@DisplayName("크리에이터 검색은 원본과 활성 JA 별칭을 함께 찾고 중복 별칭도 회원별로 한 번씩 센다")
|
||||
fun shouldSearchOriginalOrActiveJaAliasWithDistinctOwnersAndMatchingCount() {
|
||||
val viewer = saveMember("search-viewer", MemberRole.USER)
|
||||
val original = saveMember("Orion-original")
|
||||
val aliasOnly = saveMember("second-original")
|
||||
saveAlias(original, "Orion Japanese")
|
||||
saveAlias(aliasOnly, "Orion Japanese")
|
||||
entityManager.clear()
|
||||
|
||||
val items = repository.searchCreatorList("oRiOn", viewer.id!!, 0, 20)
|
||||
val page = repository.searchCreatorList("oRiOn", viewer.id!!, 1, 1)
|
||||
|
||||
assertAll(
|
||||
{ assertEquals(2, repository.searchCreatorTotalCount("oRiOn", viewer.id!!)) },
|
||||
{ assertEquals(listOf(aliasOnly.id, original.id), items.map { it.id }) },
|
||||
{ assertEquals(listOf(original.id), page.map { it.id }) },
|
||||
{
|
||||
assertEquals(
|
||||
listOf(original.id),
|
||||
repository.searchCreatorList("Orion-original", viewer.id!!, 0, 20).map { it.id }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("별칭 검색은 회원 활성·역할·태그 활성·양방향 차단 필터를 우회하지 않는다")
|
||||
fun shouldPreserveCreatorVisibilityTagAndBidirectionalBlockFiltersForAliasMatches() {
|
||||
val viewer = saveMember("filter-viewer", MemberRole.USER)
|
||||
val visible = saveMember("visible-original")
|
||||
val inactive = saveMember("inactive-original").apply { isActive = false }
|
||||
val user = saveMember("user-original", MemberRole.USER)
|
||||
val blockedByViewer = saveMember("blocked-original")
|
||||
val blockingViewer = saveMember("blocking-original")
|
||||
val inactiveTagOwner = saveMember("inactive-tag-original")
|
||||
saveBlock(viewer, blockedByViewer)
|
||||
saveBlock(blockingViewer, viewer)
|
||||
val inactiveTag = CreatorTag(tag = "inactive-tag", isActive = false)
|
||||
entityManager.persist(inactiveTag)
|
||||
entityManager.persist(MemberCreatorTag(member = inactiveTagOwner, tag = inactiveTag))
|
||||
listOf(visible, inactive, user, blockedByViewer, blockingViewer, inactiveTagOwner).forEach {
|
||||
saveAlias(it, "AliasVisibility")
|
||||
}
|
||||
entityManager.clear()
|
||||
|
||||
assertAll(
|
||||
{ assertEquals(1, repository.searchCreatorTotalCount("AliasVisibility", viewer.id!!)) },
|
||||
{
|
||||
assertEquals(
|
||||
listOf(visible.id),
|
||||
repository.searchCreatorList("AliasVisibility", viewer.id!!, 0, 20).map { it.id }
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("비활성 JA와 다른 언어 별칭은 제외하고 원본 검색과 같은 행 재활성화는 유지한다")
|
||||
fun shouldExcludeSoftClearedAndNonJaAliasesWithoutLosingOriginalSearch() {
|
||||
val viewer = saveMember("clear-viewer", MemberRole.USER)
|
||||
val creator = saveMember("current-original")
|
||||
val content = saveContent(creator, "clear content title")
|
||||
val series = saveSeries(creator, "clear series title", content)
|
||||
saveAlias(creator, "ClearedAlias", isActive = false)
|
||||
saveAlias(creator, "EnglishAlias", lang = "EN")
|
||||
entityManager.clear()
|
||||
|
||||
assertEquals(0, repository.searchCreatorTotalCount("ClearedAlias", viewer.id!!))
|
||||
assertEquals(emptyList<Long>(), repository.searchCreatorList("ClearedAlias", viewer.id!!, 0, 20).map { it.id })
|
||||
assertEquals(0, repository.searchCreatorTotalCount("EnglishAlias", viewer.id!!))
|
||||
assertEquals(1, repository.searchCreatorTotalCount("current-original", viewer.id!!))
|
||||
assertEquals(0, repository.searchContentTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL))
|
||||
assertEquals(0, repository.searchSeriesTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL))
|
||||
assertEquals(1, repository.searchContentTotalCount("current-original", viewer.id!!, false, ContentType.ALL))
|
||||
assertEquals(1, repository.searchSeriesTotalCount("current-original", viewer.id!!, false, ContentType.ALL))
|
||||
|
||||
repeat(2) {
|
||||
jdbcTemplate.update(
|
||||
"update creator_localized_name set is_active = true where member_id = ? and lang = 'JA'",
|
||||
creator.id!!
|
||||
)
|
||||
assertAll(
|
||||
{ assertEquals(1, repository.searchCreatorTotalCount("ClearedAlias", viewer.id!!)) },
|
||||
{ assertEquals(1, repository.searchContentTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL)) },
|
||||
{ assertEquals(1, repository.searchSeriesTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL)) },
|
||||
{
|
||||
assertEquals(
|
||||
listOf(content.id),
|
||||
repository.searchContentList("ClearedAlias", viewer.id!!, false, ContentType.ALL, 0, 20).map { it.id }
|
||||
)
|
||||
},
|
||||
{
|
||||
assertEquals(
|
||||
listOf(series.id),
|
||||
repository.searchSeriesList("ClearedAlias", viewer.id!!, false, ContentType.ALL, 0, 20).map { it.id }
|
||||
)
|
||||
},
|
||||
{
|
||||
assertEquals(
|
||||
listOf(creator.id),
|
||||
repository.searchCreatorList("ClearedAlias", viewer.id!!, 0, 20).map { it.id }
|
||||
)
|
||||
}
|
||||
)
|
||||
jdbcTemplate.update(
|
||||
"update creator_localized_name set is_active = false where member_id = ? and lang = 'JA'",
|
||||
creator.id!!
|
||||
)
|
||||
assertEquals(0, repository.searchCreatorTotalCount("ClearedAlias", viewer.id!!))
|
||||
assertEquals(0, repository.searchContentTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL))
|
||||
assertEquals(0, repository.searchSeriesTotalCount("ClearedAlias", viewer.id!!, false, ContentType.ALL))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("콘텐츠 검색은 소유자 별칭을 찾되 성인·활성·완성·한정·차단 필터와 콘텐츠 제목을 보존한다")
|
||||
fun shouldSearchContentOwnerAliasWithMatchingCountAndExistingContentFilters() {
|
||||
val viewer = saveMember("content-search-viewer", MemberRole.USER)
|
||||
val creator = saveMember("audio-owner-original")
|
||||
val visible = saveContent(creator, "Unchanged audio title")
|
||||
saveContent(creator, "adult audio").isAdult = true
|
||||
saveContent(creator, "inactive audio").isActive = false
|
||||
saveContent(creator, "unfinished audio").duration = null
|
||||
saveContent(creator, "limited audio", limited = 5)
|
||||
val blocked = saveMember("blocked-audio-owner")
|
||||
saveContent(blocked, "blocked audio")
|
||||
saveBlock(viewer, blocked)
|
||||
saveAlias(creator, "AudioAlias")
|
||||
saveAlias(creator, "English Audio Owner", lang = "EN")
|
||||
saveAlias(blocked, "AudioAlias")
|
||||
entityManager.clear()
|
||||
|
||||
val statistics = entityManager.entityManagerFactory.unwrap(SessionFactory::class.java).statistics
|
||||
statistics.clear()
|
||||
val items = repository.searchContentList("AudioAlias", viewer.id!!, false, ContentType.ALL, 0, 20, Lang.JA)
|
||||
val aliasQueries = statistics.queries.filter {
|
||||
Regex("""(?is)^select\s+\w+\.memberId,\s*\w+\.displayName\s+from\s+CreatorLocalizedNameEntity\b""")
|
||||
.containsMatchIn(it)
|
||||
}
|
||||
val aliasQueryCount = aliasQueries.sumOf { statistics.getQueryStatistics(it).executionCount }
|
||||
val queryCount = statistics.queryExecutionCount
|
||||
|
||||
assertAll(
|
||||
{ assertEquals(1, repository.searchContentTotalCount("AudioAlias", viewer.id!!, false, ContentType.ALL)) },
|
||||
{ assertEquals(listOf(visible.id), items.map { it.id }) },
|
||||
{ assertEquals(listOf("Unchanged audio title"), items.map { it.title }) },
|
||||
{ assertEquals(listOf("AudioAlias"), items.map { it.nickname }) },
|
||||
{ assertEquals(1, repository.searchContentTotalCount("audio-owner-original", viewer.id!!, false, ContentType.ALL)) },
|
||||
{ assertEquals(1, repository.searchContentTotalCount("Unchanged audio title", viewer.id!!, false, ContentType.ALL)) }
|
||||
)
|
||||
assertEquals(0L, aliasQueryCount, "Standalone alias SELECTs (total queries=$queryCount): $aliasQueries")
|
||||
val englishItems = repository.searchContentList("AudioAlias", viewer.id!!, false, ContentType.ALL, 0, 20, Lang.EN)
|
||||
assertEquals(listOf("English Audio Owner"), englishItems.map { it.nickname })
|
||||
assertEquals(0, repository.searchContentTotalCount("English Audio Owner", viewer.id!!, false, ContentType.ALL))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("시리즈 검색은 시리즈 소유자 별칭을 찾고 콘텐츠 조인 중복과 기존 공개 필터를 보존한다")
|
||||
fun shouldSearchSeriesOwnerAliasWithoutDuplicatingJoinedContentsOrChangingTitles() {
|
||||
val viewer = saveMember("series-search-viewer", MemberRole.USER)
|
||||
val creator = saveMember("series-owner-original")
|
||||
val audioOwner = saveMember("different-audio-owner")
|
||||
val visible = saveSeries(creator, "Unchanged series title", saveContent(audioOwner, "first episode"))
|
||||
saveSeriesContent(visible, saveContent(audioOwner, "second episode"))
|
||||
saveSeries(creator, "adult series", saveContent(creator, "public episode")).isAdult = true
|
||||
saveSeries(creator, "adult episode series", saveContent(creator, "adult episode").apply { isAdult = true })
|
||||
saveSeries(creator, "inactive series", saveContent(creator, "active episode")).isActive = false
|
||||
saveSeries(creator, "unfinished series", saveContent(creator, "unfinished episode").apply { duration = null })
|
||||
val blocked = saveMember("blocked-series-owner")
|
||||
saveSeries(blocked, "blocked series", saveContent(blocked, "blocked episode"))
|
||||
saveBlock(blocked, viewer)
|
||||
saveAlias(creator, "SeriesAlias")
|
||||
saveAlias(audioOwner, "EpisodeOwnerAlias")
|
||||
saveAlias(blocked, "SeriesAlias")
|
||||
entityManager.clear()
|
||||
|
||||
val items = repository.searchSeriesList("SeriesAlias", viewer.id!!, false, ContentType.ALL, 0, 20)
|
||||
|
||||
assertAll(
|
||||
{ assertEquals(1, repository.searchSeriesTotalCount("SeriesAlias", viewer.id!!, false, ContentType.ALL)) },
|
||||
{ assertEquals(listOf(visible.id), items.map { it.id }) },
|
||||
{ assertEquals(listOf("Unchanged series title"), items.map { it.title }) },
|
||||
{ assertEquals(0, repository.searchSeriesTotalCount("EpisodeOwnerAlias", viewer.id!!, false, ContentType.ALL)) },
|
||||
{ assertEquals(1, repository.searchSeriesTotalCount("series-owner-original", viewer.id!!, false, ContentType.ALL)) },
|
||||
{ assertEquals(1, repository.searchSeriesTotalCount("Unchanged series title", viewer.id!!, false, ContentType.ALL)) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveMember(nickname: String, role: MemberRole = MemberRole.CREATOR): Member {
|
||||
return Member(
|
||||
email = "$nickname@test.com",
|
||||
password = "password",
|
||||
nickname = nickname,
|
||||
profileImage = "$nickname.png",
|
||||
role = role
|
||||
).also { entityManager.persist(it) }
|
||||
}
|
||||
|
||||
private fun saveAlias(member: Member, displayName: String, isActive: Boolean = true, lang: String = "JA") {
|
||||
entityManager.flush()
|
||||
jdbcTemplate.update(
|
||||
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
|
||||
"values (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
||||
member.id!!,
|
||||
lang,
|
||||
displayName,
|
||||
isActive
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveBlock(member: Member, blockedMember: Member) {
|
||||
entityManager.persist(
|
||||
BlockMember(isActive = true).apply {
|
||||
this.member = member
|
||||
this.blockedMember = blockedMember
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveContent(creator: Member, title: String, limited: Int? = null): AudioContent {
|
||||
val theme = AudioContentTheme(theme = "theme-$title", image = "theme.png")
|
||||
entityManager.persist(theme)
|
||||
return AudioContent(
|
||||
title = title,
|
||||
detail = "description",
|
||||
languageCode = "ko",
|
||||
releaseDate = LocalDateTime.now().minusDays(1),
|
||||
limited = limited
|
||||
).apply {
|
||||
member = creator
|
||||
this.theme = theme
|
||||
isActive = true
|
||||
coverImage = "audio.png"
|
||||
duration = "00:10:00"
|
||||
entityManager.persist(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveSeries(creator: Member, title: String, content: AudioContent): Series {
|
||||
val genre = SeriesGenre(genre = "genre-$title", isAdult = false, isActive = true)
|
||||
entityManager.persist(genre)
|
||||
return Series(title = title, introduction = "introduction", languageCode = "ko").apply {
|
||||
member = creator
|
||||
this.genre = genre
|
||||
coverImage = "series.png"
|
||||
entityManager.persist(this)
|
||||
saveSeriesContent(this, content)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveSeriesContent(series: Series, content: AudioContent) {
|
||||
entityManager.persist(
|
||||
SeriesContent().apply {
|
||||
this.series = series
|
||||
this.content = content
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.search
|
||||
|
||||
import kr.co.vividnext.sodalive.content.ContentType
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
@@ -9,7 +10,7 @@ import org.mockito.Mockito
|
||||
|
||||
class SearchServiceTest {
|
||||
private val repository: SearchRepository = Mockito.mock(SearchRepository::class.java)
|
||||
private val service = SearchService(repository)
|
||||
private val service = SearchService(repository, LangContext())
|
||||
|
||||
@Test
|
||||
@DisplayName("콘텐츠 검색은 전달받은 isAdult 값을 그대로 사용한다")
|
||||
|
||||
+1
@@ -132,6 +132,7 @@ class LegacyCommunityPostCharacterizationTest {
|
||||
assertEquals(FcmEventType.CHANGE_NOTICE, event.type)
|
||||
assertEquals(PushNotificationCategory.COMMUNITY, event.category)
|
||||
assertEquals("creator.community.fcm.new_post", event.messageKey)
|
||||
assertEquals(creator.id, event.titleMemberId)
|
||||
assertEquals(creator.id, event.creatorId)
|
||||
assertEquals(FcmDeepLinkValue.COMMUNITY, event.deepLinkValue)
|
||||
assertEquals(creator.id, event.deepLinkId)
|
||||
|
||||
+12
-1
@@ -22,6 +22,7 @@ import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.MemberService
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
@@ -42,6 +43,7 @@ class LegacyFanTalkReplyCharacterizationTest {
|
||||
private lateinit var cheersRepository: CreatorCheersRepository
|
||||
private lateinit var applicationEventPublisher: ApplicationEventPublisher
|
||||
private lateinit var messageSource: SodaMessageSource
|
||||
private lateinit var localizedNameService: CreatorLocalizedNameService
|
||||
private lateinit var service: ExplorerService
|
||||
|
||||
@BeforeEach
|
||||
@@ -51,6 +53,7 @@ class LegacyFanTalkReplyCharacterizationTest {
|
||||
cheersRepository = Mockito.mock(CreatorCheersRepository::class.java)
|
||||
applicationEventPublisher = Mockito.mock(ApplicationEventPublisher::class.java)
|
||||
messageSource = Mockito.mock(SodaMessageSource::class.java)
|
||||
localizedNameService = Mockito.mock(CreatorLocalizedNameService::class.java)
|
||||
service = ExplorerService(
|
||||
memberService = memberService,
|
||||
memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java),
|
||||
@@ -66,6 +69,7 @@ class LegacyFanTalkReplyCharacterizationTest {
|
||||
contentTranslationRepository = Mockito.mock(ContentTranslationRepository::class.java),
|
||||
messageSource = messageSource,
|
||||
langContext = LangContext().apply { setLang(Lang.KO) },
|
||||
localizedNameService = localizedNameService,
|
||||
cloudFrontHost = "https://cdn.test"
|
||||
)
|
||||
}
|
||||
@@ -177,6 +181,8 @@ class LegacyFanTalkReplyCharacterizationTest {
|
||||
val root = createCheers(id = 300L, content = "root", member = creator, creator = creator)
|
||||
Mockito.`when`(queryRepository.getMember(creator.id!!)).thenReturn(creator)
|
||||
Mockito.`when`(queryRepository.getCreatorCheers(root.id!!)).thenReturn(root)
|
||||
Mockito.`when`(localizedNameService.findLocalizedNames(listOf(creator.id!!), Lang.KO))
|
||||
.thenReturn(mapOf(creator.id!! to "Korean creator alias"))
|
||||
var nextId = 301L
|
||||
Mockito.`when`(cheersRepository.save(Mockito.any(CreatorCheers::class.java))).thenAnswer { invocation ->
|
||||
invocation.getArgument<CreatorCheers>(0).also { cheers ->
|
||||
@@ -185,7 +191,7 @@ class LegacyFanTalkReplyCharacterizationTest {
|
||||
}
|
||||
}
|
||||
|
||||
service.writeCheers(
|
||||
val response = service.writeCheers(
|
||||
PostWriteCheersRequest(root.id, creator.id!!, "first reply", "ko"),
|
||||
creator
|
||||
)
|
||||
@@ -198,6 +204,8 @@ class LegacyFanTalkReplyCharacterizationTest {
|
||||
Mockito.verify(cheersRepository, Mockito.times(2)).save(cheersCaptor.capture())
|
||||
assertEquals(listOf("first reply", "second reply"), cheersCaptor.allValues.map { it.cheers })
|
||||
assertTrue(cheersCaptor.allValues.all { it.parent === root })
|
||||
assertEquals("duplicate-creator", response.writerNickname)
|
||||
Mockito.verifyNoInteractions(localizedNameService)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -222,6 +230,8 @@ class LegacyFanTalkReplyCharacterizationTest {
|
||||
Mockito.`when`(queryRepository.getMember(creator.id!!)).thenReturn(creator)
|
||||
Mockito.`when`(memberService.isBlocked(writer.id!!, creator.id!!)).thenReturn(true)
|
||||
Mockito.`when`(messageSource.getMessage("explorer.creator.blocked_cheers", Lang.KO)).thenReturn("blocked %s")
|
||||
Mockito.`when`(localizedNameService.findLocalizedNames(listOf(creator.id!!), Lang.KO))
|
||||
.thenReturn(mapOf(creator.id!! to "Korean blocked alias"))
|
||||
|
||||
val exception = assertThrows(SodaException::class.java) {
|
||||
service.writeCheers(PostWriteCheersRequest(creatorId = creator.id!!, content = "reply"), writer)
|
||||
@@ -229,6 +239,7 @@ class LegacyFanTalkReplyCharacterizationTest {
|
||||
|
||||
assertNull(exception.messageKey)
|
||||
assertEquals("blocked blocked-creator", exception.message)
|
||||
Mockito.verifyNoInteractions(localizedNameService)
|
||||
Mockito.verifyNoInteractions(cheersRepository, applicationEventPublisher)
|
||||
}
|
||||
|
||||
|
||||
+467
@@ -0,0 +1,467 @@
|
||||
package kr.co.vividnext.sodalive.v2.creatorname.application
|
||||
|
||||
import kr.co.vividnext.sodalive.audition.Audition
|
||||
import kr.co.vividnext.sodalive.audition.AuditionApplicant
|
||||
import kr.co.vividnext.sodalive.audition.AuditionRole
|
||||
import kr.co.vividnext.sodalive.audition.applicant.AuditionApplicantService
|
||||
import kr.co.vividnext.sodalive.audition.applicant.AuditionApplicantSortType
|
||||
import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront
|
||||
import kr.co.vividnext.sodalive.can.CanService
|
||||
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
|
||||
import kr.co.vividnext.sodalive.can.use.CanUsage
|
||||
import kr.co.vividnext.sodalive.can.use.UseCan
|
||||
import kr.co.vividnext.sodalive.can.use.UseCanCalculate
|
||||
import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus
|
||||
import kr.co.vividnext.sodalive.common.CountryContext
|
||||
import kr.co.vividnext.sodalive.content.AudioContent
|
||||
import kr.co.vividnext.sodalive.content.AudioContentService
|
||||
import kr.co.vividnext.sodalive.content.order.Order
|
||||
import kr.co.vividnext.sodalive.content.order.OrderService
|
||||
import kr.co.vividnext.sodalive.content.order.OrderType
|
||||
import kr.co.vividnext.sodalive.content.playlist.AudioContentPlaylist
|
||||
import kr.co.vividnext.sodalive.content.playlist.AudioContentPlaylistRedisRepository
|
||||
import kr.co.vividnext.sodalive.content.playlist.AudioContentPlaylistService
|
||||
import kr.co.vividnext.sodalive.content.playlist.PlaylistContentIdAndOrder
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
|
||||
import kr.co.vividnext.sodalive.explorer.ExplorerService
|
||||
import kr.co.vividnext.sodalive.explorer.MemberDonationRankingListResponse
|
||||
import kr.co.vividnext.sodalive.explorer.MemberDonationRankingResponse
|
||||
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationMessage
|
||||
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationService
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityService
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.CreatorCommunityComment
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||
import kr.co.vividnext.sodalive.member.DonationRankingPeriod
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.MemberService
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMember
|
||||
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNotEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.mock.mockito.MockBean
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.data.redis.core.RedisTemplate
|
||||
import org.springframework.jdbc.core.JdbcTemplate
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@SpringBootTest(
|
||||
properties = [
|
||||
"spring.cache.type=none",
|
||||
"cloud.aws.cloud-front.host=https://cdn.test",
|
||||
"spring.datasource.url=jdbc:h2:mem:legacy-current-name-reads;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE"
|
||||
]
|
||||
)
|
||||
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
|
||||
@Transactional
|
||||
class LegacyCurrentNameReadIntegrationTest @Autowired constructor(
|
||||
private val entityManager: EntityManager,
|
||||
private val jdbcTemplate: JdbcTemplate,
|
||||
private val memberService: MemberService,
|
||||
private val orderService: OrderService,
|
||||
private val playlistService: AudioContentPlaylistService,
|
||||
private val playlistRepository: AudioContentPlaylistRedisRepository,
|
||||
private val communityService: CreatorCommunityService,
|
||||
private val donationService: ChannelDonationService,
|
||||
private val explorerService: ExplorerService,
|
||||
private val canService: CanService,
|
||||
private val contentService: AudioContentService,
|
||||
private val applicantService: AuditionApplicantService,
|
||||
private val redisTemplate: RedisTemplate<String, Any>
|
||||
) {
|
||||
@MockBean
|
||||
private lateinit var langContext: LangContext
|
||||
|
||||
@MockBean
|
||||
private lateinit var countryContext: CountryContext
|
||||
|
||||
@MockBean
|
||||
private lateinit var cloudFront: AudioContentCloudFront
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
jdbcTemplate.execute(
|
||||
"CREATE ALIAS IF NOT EXISTS DATE_FORMAT FOR 'kr.co.vividnext.sodalive.support.H2MysqlDateFunctions.dateFormat'"
|
||||
)
|
||||
jdbcTemplate.execute(
|
||||
"CREATE ALIAS IF NOT EXISTS CONVERT_TZ FOR 'kr.co.vividnext.sodalive.support.H2MysqlDateFunctions.convertTz'"
|
||||
)
|
||||
Mockito.doReturn(Lang.JA).`when`(langContext).lang
|
||||
Mockito.doReturn("US").`when`(countryContext).countryCode
|
||||
Mockito.doReturn("https://cdn.test/signed-audio").`when`(cloudFront)
|
||||
.generateSignedURL(Mockito.anyString(), Mockito.anyLong())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("다른 회원 프로필과 차단 목록은 JA 이름을 표시하고 편집용 원본 프로필과 USER는 보존한다")
|
||||
fun shouldLocalizeMemberDisplayAndBlockedListWithoutChangingRawEditorOrUserName() {
|
||||
val viewer = saveMember("blocked-viewer", MemberRole.USER)
|
||||
val creator = saveMember("blocked-creator", MemberRole.CREATOR)
|
||||
val user = saveMember("blocked-user", MemberRole.USER)
|
||||
listOf(creator, user).forEach { target ->
|
||||
entityManager.persist(
|
||||
BlockMember().apply {
|
||||
member = viewer
|
||||
blockedMember = target
|
||||
}
|
||||
)
|
||||
}
|
||||
saveAlias(creator, "Japanese blocked creator")
|
||||
saveAlias(user, "Must not replace blocked USER")
|
||||
entityManager.clear()
|
||||
|
||||
assertEquals("Japanese blocked creator", memberService.getMemberProfile(creator.id!!, viewer.id!!).nickname)
|
||||
assertEquals("blocked-creator", memberService.getMember(creator.id!!, "web").nickname)
|
||||
assertEquals("Japanese blocked creator", memberService.getMyPage(creator, "web").nickname)
|
||||
val blocked = memberService.getBlockedMemberList(viewer.id!!, 0, 20)
|
||||
assertEquals(2, blocked.totalCount)
|
||||
assertEquals(
|
||||
mapOf(creator.id to "Japanese blocked creator", user.id to "blocked-user"),
|
||||
blocked.items.associate { it.memberId to it.nickname }
|
||||
)
|
||||
assertEquals("blocked-creator", entityManager.find(Member::class.java, creator.id).nickname)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("주문과 Redis 플레이리스트 상세는 콘텐츠 ID와 다른 소유자 이름을 현재 언어로 표시한다")
|
||||
fun shouldLocalizeOrderAndPlaylistOwnersWithoutChangingTitlesOrStoredPlaylist() {
|
||||
val viewer = saveMember("playlist-viewer", MemberRole.USER)
|
||||
val creator = saveMember("playlist-creator", MemberRole.CREATOR)
|
||||
val content = saveContent(creator, "Unchanged playlist title")
|
||||
entityManager.persist(
|
||||
Order(type = OrderType.KEEP).apply {
|
||||
member = viewer
|
||||
this.creator = creator
|
||||
audioContent = content
|
||||
}
|
||||
)
|
||||
saveAlias(creator, "Japanese playlist creator")
|
||||
entityManager.clear()
|
||||
assertNotEquals(creator.id, content.id)
|
||||
val playlist = AudioContentPlaylist(
|
||||
id = 100000L + viewer.id!!,
|
||||
memberId = viewer.id!!,
|
||||
title = "playlist metadata",
|
||||
contentIdAndOrderList = listOf(PlaylistContentIdAndOrder(content.id!!, 1))
|
||||
)
|
||||
playlistRepository.save(playlist)
|
||||
try {
|
||||
val order = orderService.getAudioContentOrderList(viewer, 0, 20)
|
||||
assertEquals("Japanese playlist creator", order.items.single().creatorNickname)
|
||||
assertEquals("Unchanged playlist title", order.items.single().title)
|
||||
val detail = playlistService.getPlaylistDetail(playlist.id, viewer)
|
||||
assertEquals("Japanese playlist creator", detail.contentList.single().creatorNickname)
|
||||
assertEquals("Unchanged playlist title", detail.contentList.single().title)
|
||||
assertEquals(playlist, playlistRepository.findById(playlist.id).orElseThrow())
|
||||
Mockito.doReturn(Lang.KO).`when`(langContext).lang
|
||||
val korean = playlistService.getPlaylistDetail(playlist.id, viewer)
|
||||
assertEquals("playlist-creator", korean.contentList.single().creatorNickname)
|
||||
assertEquals("Japanese playlist creator", detail.contentList.single().creatorNickname)
|
||||
} finally {
|
||||
playlistRepository.delete(playlist)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("한정 콘텐츠 상세의 구매자 목록은 콘텐츠 소유자가 아닌 각 구매자 CREATOR 이름만 대체한다")
|
||||
fun shouldLocalizeMixedBuyerNamesByBuyerIdWithoutAddingPublicOwnerFields() {
|
||||
val owner = saveMember("buyer-content-owner", MemberRole.CREATOR)
|
||||
val creatorBuyer = saveMember("buyer-creator", MemberRole.CREATOR)
|
||||
val userBuyer = saveMember("buyer-user", MemberRole.USER)
|
||||
val content = saveContent(owner, "Unchanged limited content title", limited = 5)
|
||||
listOf(creatorBuyer, userBuyer).forEach { buyer ->
|
||||
entityManager.persist(
|
||||
Order(type = OrderType.KEEP).apply {
|
||||
member = buyer
|
||||
creator = owner
|
||||
audioContent = content
|
||||
}
|
||||
)
|
||||
}
|
||||
saveAlias(owner, "Japanese content owner")
|
||||
saveAlias(creatorBuyer, "Japanese buyer")
|
||||
saveAlias(userBuyer, "Must not replace buyer USER")
|
||||
entityManager.clear()
|
||||
|
||||
val detail = contentService.getDetail(content.id!!, owner, false, "Asia/Seoul")
|
||||
|
||||
assertEquals("Japanese content owner", detail.creator.nickname)
|
||||
assertEquals(setOf("Japanese buyer", "buyer-user"), detail.buyerList.map { it.nickname }.toSet())
|
||||
assertEquals("Unchanged limited content title", detail.title)
|
||||
assertEquals("https://cdn.test/signed-audio", detail.contentUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("레거시 커뮤니티 게시글·댓글·답글·구매 응답은 활성 JA·KO 별칭이 있어도 원본 이름을 유지한다")
|
||||
fun shouldKeepOriginalCommunityNamesDespiteActiveJaAndKoAliases() {
|
||||
val viewer = saveMember("community-viewer", MemberRole.USER)
|
||||
val creator = saveMember("community-owner", MemberRole.CREATOR)
|
||||
val peer = saveMember("deleted_community-peer", MemberRole.CREATOR)
|
||||
val post = savePost(creator)
|
||||
val parent = CreatorCommunityComment(comment = "unchanged creator comment").apply {
|
||||
member = peer
|
||||
creatorCommunity = post
|
||||
entityManager.persist(this)
|
||||
}
|
||||
val reply = CreatorCommunityComment(comment = "unchanged USER reply").apply {
|
||||
member = viewer
|
||||
creatorCommunity = post
|
||||
this.parent = parent
|
||||
entityManager.persist(this)
|
||||
}
|
||||
val creatorReply = CreatorCommunityComment(comment = "unchanged creator reply").apply {
|
||||
member = creator
|
||||
creatorCommunity = post
|
||||
this.parent = parent
|
||||
entityManager.persist(this)
|
||||
}
|
||||
saveReceipt(viewer, CanUsage.PAID_COMMUNITY_POST, creator).communityPost = post
|
||||
saveAlias(creator, "Japanese community owner")
|
||||
saveAlias(peer, "Japanese community writer")
|
||||
saveAlias(viewer, "Must not replace community USER")
|
||||
listOf(creator, peer).forEach { writer ->
|
||||
jdbcTemplate.update(
|
||||
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
|
||||
"values (?, 'KO', ?, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
||||
writer.id!!,
|
||||
"Korean ${writer.nickname}"
|
||||
)
|
||||
}
|
||||
entityManager.clear()
|
||||
|
||||
listOf(Lang.JA, Lang.KO).forEach { lang ->
|
||||
Mockito.doReturn(lang).`when`(langContext).lang
|
||||
val posts = communityService.getCommunityPostList(creator.id!!, viewer.id!!, "Asia/Seoul", 0, 20, false)
|
||||
assertEquals("community-owner", posts.single().creatorNickname)
|
||||
assertEquals("community-peer", posts.single().firstComment!!.nickname)
|
||||
assertEquals("unchanged post content", posts.single().content)
|
||||
val detail = communityService.getCommunityPostDetail(post.id!!, viewer.id!!, "Asia/Seoul", false)
|
||||
assertEquals("community-owner", detail.creatorNickname)
|
||||
val comments = communityService.getCommunityPostCommentList(post.id!!, viewer.id!!, "Asia/Seoul", 0, 20, false)
|
||||
assertEquals("community-peer", comments.items.single().nickname)
|
||||
val replies = communityService.getCommentReplyList(parent.id!!, viewer.id!!, "Asia/Seoul", 0, 20, false)
|
||||
assertEquals(
|
||||
mapOf(reply.id to "community-viewer", creatorReply.id to "community-owner"),
|
||||
replies.items.associate { it.id to it.nickname }
|
||||
)
|
||||
assertEquals("unchanged USER reply", replies.items.single { it.id == reply.id }.comment)
|
||||
val purchased = communityService.purchasePost(post.id!!, viewer.id!!, "Asia/Seoul", false, "web")
|
||||
assertEquals("community-owner", purchased.creatorNickname)
|
||||
assertEquals("community-peer", purchased.firstComment!!.nickname)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("채널 후원 목록의 이름 소유자는 채널이 아니라 후원자이며 USER는 원본이다")
|
||||
fun shouldLocalizeCreatorDonorsIndependentlyOfTheChannelOwner() {
|
||||
val viewer = saveMember("donation-viewer", MemberRole.USER)
|
||||
val creator = saveMember("donation-channel", MemberRole.CREATOR)
|
||||
val donor = saveMember("donation-creator-donor", MemberRole.CREATOR)
|
||||
listOf(viewer, donor).forEach { writer ->
|
||||
entityManager.persist(
|
||||
ChannelDonationMessage(can = 100, isSecret = false).apply {
|
||||
member = writer
|
||||
this.creator = creator
|
||||
}
|
||||
)
|
||||
}
|
||||
saveAlias(creator, "Wrong channel owner name")
|
||||
saveAlias(donor, "Japanese donor")
|
||||
saveAlias(viewer, "Must not replace donor USER")
|
||||
entityManager.clear()
|
||||
|
||||
val response = donationService.getChannelDonationList(creator.id!!, viewer, 0, 20)
|
||||
assertEquals(2, response.totalCount)
|
||||
assertEquals(
|
||||
mapOf(donor.id to "Japanese donor", viewer.id to "donation-viewer"),
|
||||
response.items.associate { it.memberId to it.nickname }
|
||||
)
|
||||
assertEquals(listOf(100, 100), response.items.map { it.can })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("receipt는 각 recipient·audio·community 소유자의 현재 이름을 쓰고 리소스 제목과 금액을 보존한다")
|
||||
fun shouldLocalizeReceiptOwnerNamesWithoutReplacingResourceTitlesOrAmounts() {
|
||||
val viewer = saveMember("receipt-viewer", MemberRole.USER)
|
||||
val owner = saveMember("receipt-owner", MemberRole.CREATOR)
|
||||
val recipient = saveMember("receipt-recipient", MemberRole.CREATOR)
|
||||
val content = saveContent(owner, "Unchanged receipt content title")
|
||||
val post = savePost(owner)
|
||||
saveReceipt(viewer, CanUsage.DONATION, owner).audioContent = content
|
||||
saveReceipt(viewer, CanUsage.PAID_COMMUNITY_POST, owner).communityPost = post
|
||||
saveReceipt(viewer, CanUsage.CHANNEL_DONATION, recipient)
|
||||
saveReceipt(viewer, CanUsage.CHANNEL_DONATION, viewer)
|
||||
saveReceipt(viewer, CanUsage.ORDER_CONTENT, owner).audioContent = content
|
||||
saveAlias(owner, "Japanese receipt owner")
|
||||
saveAlias(recipient, "Japanese recipient")
|
||||
saveAlias(viewer, "Must not replace receipt USER")
|
||||
entityManager.clear()
|
||||
|
||||
val rows = canService.getCanUseStatus(viewer, PageRequest.of(0, 20), "UTC", "web")
|
||||
assertEquals(
|
||||
setOf(
|
||||
"[콘텐츠 후원] Japanese receipt owner",
|
||||
"[게시글 보기] Japanese receipt owner",
|
||||
"[채널 후원] Japanese recipient",
|
||||
"[채널 후원] receipt-viewer",
|
||||
"[콘텐츠 구매] Unchanged receipt content title"
|
||||
),
|
||||
rows.map { it.title }.toSet()
|
||||
)
|
||||
assertEquals(5, rows.size)
|
||||
assertEquals(listOf(100, 100, 100, 100, 100), rows.map { it.can })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("오디션 지원자는 applicant ID가 아닌 member ID로 이름을 표시하고 USER·count·정렬을 보존한다")
|
||||
fun shouldLocalizeAuditionApplicantsByMemberIdAndKeepUserNamesAndCount() {
|
||||
saveMember("applicant-unused-viewer", MemberRole.USER)
|
||||
val creator = saveMember("applicant-creator", MemberRole.CREATOR)
|
||||
val user = saveMember("applicant-user", MemberRole.USER)
|
||||
val audition = Audition(title = "Unchanged audition title", information = "information")
|
||||
entityManager.persist(audition)
|
||||
val role = AuditionRole(name = "Unchanged role name", information = "information").apply {
|
||||
this.audition = audition
|
||||
entityManager.persist(this)
|
||||
}
|
||||
val creatorApplicant = AuditionApplicant(phoneNumber = "01000000000", voicePath = "voice.m4a").apply {
|
||||
member = creator
|
||||
this.role = role
|
||||
entityManager.persist(this)
|
||||
}
|
||||
val userApplicant = AuditionApplicant(phoneNumber = "01000000001", voicePath = "voice.m4a").apply {
|
||||
member = user
|
||||
this.role = role
|
||||
entityManager.persist(this)
|
||||
}
|
||||
saveAlias(creator, "Japanese applicant")
|
||||
saveAlias(user, "Must not replace applicant USER")
|
||||
entityManager.clear()
|
||||
assertNotEquals(creator.id, creatorApplicant.id)
|
||||
|
||||
val response = applicantService.getAuditionApplicantList(role.id!!, AuditionApplicantSortType.NEWEST, 0, 20)
|
||||
|
||||
assertEquals(2, response.totalCount)
|
||||
assertEquals(listOf(userApplicant.id, creatorApplicant.id), response.items.map { it.applicantId })
|
||||
assertEquals(listOf("applicant-user", "Japanese applicant"), response.items.map { it.nickname })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("중립 후원랭킹 cache hit는 요청별 현재 이름을 복사하고 원본 변경·해제 후 현재 원본으로 돌아간다")
|
||||
fun shouldRenderCachedDonorNamesAfterReadAndKeepCachedRowsImmutable() {
|
||||
val viewer = saveMember("cached-viewer", MemberRole.USER)
|
||||
val channel = saveMember("cached-channel", MemberRole.CREATOR)
|
||||
val donor = saveMember("cached-donor-original", MemberRole.CREATOR)
|
||||
saveAlias(donor, "Japanese cached donor")
|
||||
entityManager.clear()
|
||||
val key = "creator_donation_ranking_v2:${channel.id}:CUMULATIVE:0:20:false"
|
||||
val totalKey = "creator_donation_ranking_member_total_v2:${channel.id}:CUMULATIVE"
|
||||
val cached = MemberDonationRankingListResponse(
|
||||
listOf(MemberDonationRankingResponse(donor.id!!, "stale cached original", "image.png", 0))
|
||||
)
|
||||
redisTemplate.opsForValue().set(key, cached)
|
||||
redisTemplate.opsForValue().set(totalKey, 1)
|
||||
try {
|
||||
val japanese = explorerService.getCreatorProfileDonationRanking(
|
||||
channel.id!!,
|
||||
DonationRankingPeriod.CUMULATIVE,
|
||||
PageRequest.of(0, 20),
|
||||
viewer
|
||||
)
|
||||
assertEquals("Japanese cached donor", japanese.userDonationRanking.single().nickname)
|
||||
jdbcTemplate.update("update member set nickname = ? where id = ?", "renamed donor", donor.id!!)
|
||||
jdbcTemplate.update("update creator_localized_name set is_active = false where member_id = ?", donor.id!!)
|
||||
entityManager.clear()
|
||||
val cleared = explorerService.getCreatorProfileDonationRanking(
|
||||
channel.id!!,
|
||||
DonationRankingPeriod.CUMULATIVE,
|
||||
PageRequest.of(0, 20),
|
||||
viewer
|
||||
)
|
||||
assertEquals("renamed donor", cleared.userDonationRanking.single().nickname)
|
||||
assertEquals("Japanese cached donor", japanese.userDonationRanking.single().nickname)
|
||||
assertEquals(cached, redisTemplate.opsForValue().get(key))
|
||||
} finally {
|
||||
redisTemplate.delete(listOf(key, totalKey))
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveMember(nickname: String, role: MemberRole): Member =
|
||||
Member(
|
||||
email = "$nickname@test.com",
|
||||
password = "password",
|
||||
nickname = nickname,
|
||||
profileImage = "profile.png",
|
||||
role = role
|
||||
).also { entityManager.persist(it) }
|
||||
|
||||
private fun saveContent(owner: Member, title: String, limited: Int? = null): AudioContent {
|
||||
val theme = AudioContentTheme(theme = "theme-$title", image = "theme.png")
|
||||
entityManager.persist(theme)
|
||||
return AudioContent(
|
||||
title = title,
|
||||
detail = "detail",
|
||||
languageCode = "ko",
|
||||
releaseDate = LocalDateTime.now().minusDays(1),
|
||||
limited = limited,
|
||||
remaining = limited
|
||||
)
|
||||
.apply {
|
||||
member = owner
|
||||
this.theme = theme
|
||||
isActive = true
|
||||
content = "output/audio.m4a"
|
||||
duration = "00:10:00"
|
||||
coverImage = "content.png"
|
||||
entityManager.persist(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun savePost(owner: Member): CreatorCommunity =
|
||||
CreatorCommunity(
|
||||
content = "unchanged post content",
|
||||
price = 0,
|
||||
isCommentAvailable = true,
|
||||
isAdult = false,
|
||||
languageCode = "ko"
|
||||
).apply {
|
||||
member = owner
|
||||
entityManager.persist(this)
|
||||
}
|
||||
|
||||
private fun saveReceipt(viewer: Member, usage: CanUsage, recipient: Member): UseCan {
|
||||
entityManager.flush()
|
||||
val receipt = UseCan(canUsage = usage, can = 100, rewardCan = 0).apply {
|
||||
member = viewer
|
||||
entityManager.persist(this)
|
||||
}
|
||||
entityManager.persist(
|
||||
UseCanCalculate(100, PaymentGateway.PG, UseCanCalculateStatus.RECEIVED).apply {
|
||||
useCan = receipt
|
||||
recipientCreatorId = recipient.id!!
|
||||
}
|
||||
)
|
||||
return receipt
|
||||
}
|
||||
|
||||
private fun saveAlias(member: Member, name: String) {
|
||||
entityManager.flush()
|
||||
jdbcTemplate.update(
|
||||
"insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at) " +
|
||||
"values (?, 'JA', ?, true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
||||
member.id!!,
|
||||
name
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user