feat(creatorname): v2 조회와 채팅에 언어별 이름을 적용한다

This commit is contained in:
2026-10-16 14:05:04 +09:00
parent eef2e2bf55
commit e264f5d062
154 changed files with 2777 additions and 989 deletions
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.content.all.adapter.`in`.web
import kr.co.vividnext.sodalive.common.CountryContext
import kr.co.vividnext.sodalive.configs.SecurityConfig
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.jwt.JwtAccessDeniedHandler
@@ -14,6 +15,7 @@ import kr.co.vividnext.sodalive.v2.api.content.all.application.MainContentAllFac
import kr.co.vividnext.sodalive.v2.api.content.all.dto.MainContentAllTabResponse
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllType
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
@@ -41,6 +43,11 @@ class MainContentAllControllerTest @Autowired constructor(
@MockBean
private lateinit var langContext: LangContext
@BeforeEach
fun setUpLanguage() {
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
}
@MockBean
private lateinit var sodaMessageSource: SodaMessageSource
@@ -57,7 +64,7 @@ class MainContentAllControllerTest @Autowired constructor(
@DisplayName("전체 탭 조회는 비회원에게 200 OK와 기본 응답을 반환한다")
fun shouldAllowAnonymousAndUseDefaultType() {
Mockito.doReturn(response(MainContentAllType.AUDIO, ContentSort.LATEST)).`when`(facade)
.getContents(null, null, null, null, null, null)
.getContents(null, null, null, null, null, null, Lang.KO)
mockMvc.perform(get("/api/v2/audio/contents"))
.andExpect(status().isOk)
@@ -76,7 +83,7 @@ class MainContentAllControllerTest @Autowired constructor(
role = MemberRole.USER
).apply { id = 10L }
Mockito.doReturn(response(MainContentAllType.SERIES, ContentSort.POPULAR)).`when`(facade)
.getContents("SERIES", "POPULAR", "MON", 1, 30, member)
.getContents("SERIES", "POPULAR", "MON", 1, 30, member, Lang.KO)
mockMvc.perform(
get("/api/v2/audio/contents")
@@ -92,7 +99,7 @@ class MainContentAllControllerTest @Autowired constructor(
.andExpect(jsonPath("$.data.type").value("SERIES"))
.andExpect(jsonPath("$.data.sort").value("POPULAR"))
Mockito.verify(facade).getContents("SERIES", "POPULAR", "MON", 1, 30, member)
Mockito.verify(facade).getContents("SERIES", "POPULAR", "MON", 1, 30, member, Lang.KO)
}
private fun response(type: MainContentAllType, sort: ContentSort): MainContentAllTabResponse {
@@ -10,12 +10,16 @@ import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDays
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
import org.hamcrest.Matchers.contains
import org.hamcrest.Matchers.nullValue
import org.hibernate.SessionFactory
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.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.web.servlet.MockMvc
@@ -30,6 +34,7 @@ import javax.persistence.EntityManager
properties = [
"cloud.aws.cloud-front.host=https://cdn.test",
"spring.cache.type=none",
"spring.jpa.properties.hibernate.generate_statistics=true",
"spring.datasource.url=jdbc:h2:mem:main-content-all-e2e;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE"
]
)
@@ -39,8 +44,107 @@ import javax.persistence.EntityManager
class MainContentAllEndToEndTest @Autowired constructor(
private val mockMvc: MockMvc,
private val entityManager: EntityManager,
private val transactionTemplate: TransactionTemplate
private val transactionTemplate: TransactionTemplate,
private val jdbcTemplate: JdbcTemplate
) {
@Test
@DisplayName("전체 콘텐츠 API는 JA 별칭을 우선하고 KO 별칭 미등록 시 일본어 원본을, 별칭 없는 작성자는 현재 원본을 반환한다")
fun shouldSelectLocalizedCreatorNamesWithoutChangingAudioTitles() {
transactionTemplate.executeWithoutResult { saveMember("main-all-resource-id-control", MemberRole.USER) }
val localized = createAudioFixture("main-all-localized")
val fallback = createAudioFixture("main-all-fallback")
transactionTemplate.executeWithoutResult {
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)
""".trimIndent(),
localized.creatorId,
"日本語クリエイター"
)
entityManager.find(Member::class.java, localized.creatorId).nickname = "改名さくら"
}
mockMvc.perform(get("/api/v2/audio/contents").param("type", "AUDIO").header("Accept-Language", "ko"))
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.totalCount").value(2))
.andExpect(
jsonPath("$.data.audios[?(@.audioContentId == ${localized.audioContentId})].creatorNickname")
.value(contains("改名さくら"))
)
val statistics = entityManager.entityManagerFactory.unwrap(SessionFactory::class.java).statistics
statistics.clear()
mockMvc.perform(get("/api/v2/audio/contents").param("type", "AUDIO").header("Accept-Language", "ja"))
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.totalCount").value(2))
.andExpect(
jsonPath("$.data.audios[?(@.audioContentId == ${localized.audioContentId})].creatorNickname")
.value(contains("日本語クリエイター"))
)
.andExpect(
jsonPath("$.data.audios[?(@.audioContentId == ${fallback.audioContentId})].creatorNickname")
.value(contains("main-all-fallback-creator"))
)
.andExpect(
jsonPath("$.data.audios[?(@.audioContentId == ${localized.audioContentId})].title")
.value(contains("main-all-localized-audio"))
)
.andExpect(jsonPath("$.data.audios[0].creatorId").doesNotExist())
.andExpect(jsonPath("$.data.audios[0].creatorMemberId").doesNotExist())
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 }
assertEquals(
0L,
aliasQueryCount,
"Standalone alias SELECTs (total queries=${statistics.queryExecutionCount}): $aliasQueries"
)
}
@Test
@DisplayName("영어 활성 이름도 JOIN으로 읽으며 비활성 이름은 원본으로 fallback하고 인기순 집계를 유지한다")
fun shouldReadActiveEnglishCreatorNameAndFallbackWhenInactive() {
val active = createAudioFixture("main-all-english-active")
val inactive = createAudioFixture("main-all-english-inactive")
transactionTemplate.executeWithoutResult {
listOf(Triple(active.creatorId, "English creator", true), Triple(inactive.creatorId, "Inactive name", false))
.forEach { (creatorId, name, isActive) ->
jdbcTemplate.update(
"""
insert into creator_localized_name (member_id, lang, display_name, is_active, created_at, updated_at)
values (?, 'EN', ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
""".trimIndent(),
creatorId,
name,
isActive
)
}
}
mockMvc.perform(
get("/api/v2/audio/contents").param("type", "AUDIO").param("sort", "POPULAR").header("Accept-Language", "en")
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.totalCount").value(2))
.andExpect(jsonPath("$.data.audios.length()").value(2))
.andExpect(
jsonPath("$.data.audios[?(@.audioContentId == ${active.audioContentId})].creatorNickname")
.value(contains("English creator"))
)
.andExpect(
jsonPath("$.data.audios[?(@.audioContentId == ${inactive.audioContentId})].creatorNickname")
.value(contains("main-all-english-inactive-creator"))
)
.andExpect(
jsonPath("$.data.audios[?(@.audioContentId == ${active.audioContentId})].title")
.value(contains("main-all-english-active-audio"))
)
}
@Test
@DisplayName("전체 탭 AUDIO API는 controller-service-repository를 거쳐 오디오 응답과 빈 series를 반환한다")
fun shouldReturnAudioContentsThroughControllerServiceAndRepository() {
@@ -196,7 +300,7 @@ class MainContentAllEndToEndTest @Autowired constructor(
entityManager.flush()
entityManager.clear()
AudioFixture(audioContentId = audio.id!!)
AudioFixture(audioContentId = audio.id!!, creatorId = creator.id!!)
}!!
}
@@ -308,7 +412,8 @@ class MainContentAllEndToEndTest @Autowired constructor(
}
private data class AudioFixture(
val audioContentId: Long
val audioContentId: Long,
val creatorId: Long
)
private data class SeriesFixture(
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.content.all.application
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.v2.common.domain.ContentSort
@@ -36,7 +37,7 @@ class MainContentAllFacadeTest {
page = MainContentPage(1, 30),
hasNext = false
)
).`when`(queryService).getContents("FREE", "PRICE_LOW", "MON", 1, 30, member)
).`when`(queryService).getContents("FREE", "PRICE_LOW", "MON", 1, 30, member, Lang.KO)
val response = facade.getContents(
type = "FREE",
@@ -44,10 +45,11 @@ class MainContentAllFacadeTest {
dayOfWeek = "MON",
page = 1,
size = 30,
member = member
member = member,
lang = Lang.KO
)
Mockito.verify(queryService).getContents("FREE", "PRICE_LOW", "MON", 1, 30, member)
Mockito.verify(queryService).getContents("FREE", "PRICE_LOW", "MON", 1, 30, member, Lang.KO)
assertEquals(MainContentAllType.FREE, response.type)
assertEquals(ContentSort.PRICE_LOW, response.sort)
}
@@ -27,6 +27,7 @@ class MainContentAllTabResponseTest {
audios = emptyList(),
series = listOf(
MainContentAllSeries(
creatorMemberId = 100L,
seriesId = 10L,
title = "시리즈",
coverImageUrl = "https://cdn/series.jpg",
@@ -58,6 +59,7 @@ class MainContentAllTabResponseTest {
totalCount = 1,
audios = listOf(
MainContentAllAudio(
creatorMemberId = 100L,
audioContentId = 1L,
title = "audio",
imageUrl = "https://cdn/audio.jpg",
@@ -69,6 +71,7 @@ class MainContentAllTabResponseTest {
creatorNickname = "creator"
),
MainContentAllAudio(
creatorMemberId = 100L,
audioContentId = 2L,
title = "free audio",
imageUrl = "https://cdn/free-audio.jpg",
@@ -80,6 +83,7 @@ class MainContentAllTabResponseTest {
creatorNickname = "creator"
),
MainContentAllAudio(
creatorMemberId = 100L,
audioContentId = 3L,
title = "point unavailable audio",
imageUrl = "https://cdn/point-unavailable-audio.jpg",
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.content.overview.adapter.`in`.web
import kr.co.vividnext.sodalive.common.CountryContext
import kr.co.vividnext.sodalive.configs.SecurityConfig
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.jwt.JwtAccessDeniedHandler
@@ -13,6 +14,7 @@ import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.v2.api.content.overview.application.ContentOverviewFacade
import kr.co.vividnext.sodalive.v2.api.content.overview.dto.ContentOverviewPageResponse
import kr.co.vividnext.sodalive.v2.api.content.overview.dto.ContentOverviewType
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
@@ -41,6 +43,11 @@ class ContentOverviewControllerTest @Autowired constructor(
@MockBean
private lateinit var langContext: LangContext
@BeforeEach
fun setUpLanguage() {
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
}
@MockBean
private lateinit var sodaMessageSource: SodaMessageSource
@@ -62,7 +69,7 @@ class ContentOverviewControllerTest @Autowired constructor(
fun shouldPassAuthenticatedMemberAndQueryParameters() {
val member = member(id = 10L)
Mockito.doReturn(emptyResponse(ContentOverviewType.FIRST_AUDIO_CONTENT)).`when`(facade)
.getContents(eqValue("FIRST_AUDIO_CONTENT"), eqValue(1), eqValue(30), eqValue(member))
.getContents(eqValue("FIRST_AUDIO_CONTENT"), eqValue(1), eqValue(30), eqValue(member), eqValue(Lang.KO))
mockMvc.perform(
get("/api/v2/contents")
@@ -75,7 +82,8 @@ class ContentOverviewControllerTest @Autowired constructor(
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.type").value("FIRST_AUDIO_CONTENT"))
Mockito.verify(facade).getContents(eqValue("FIRST_AUDIO_CONTENT"), eqValue(1), eqValue(30), eqValue(member))
Mockito.verify(facade)
.getContents(eqValue("FIRST_AUDIO_CONTENT"), eqValue(1), eqValue(30), eqValue(member), eqValue(Lang.KO))
}
private fun <T> eqValue(value: T): T {
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.content.overview.application
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.contentpreference.MemberContentPreferenceService
@@ -31,9 +32,9 @@ class ContentOverviewFacadeTest {
fun shouldReturnNewAndHotPage() {
val member = member(id = 10L)
Mockito.doReturn((1L..21L).map { audioCard(it) }).`when`(audioRecommendationQueryService)
.findNewAndHotAudios(member, offset = 0L, limit = 21)
.findNewAndHotAudios(member, offset = 0L, limit = 21, lang = Lang.KO)
val response = facade.getContents("NEW_AND_HOT_AUDIO", page = 0, size = 20, member = member)
val response = facade.getContents("NEW_AND_HOT_AUDIO", page = 0, size = 20, member = member, lang = Lang.KO)
assertEquals(ContentOverviewType.NEW_AND_HOT_AUDIO, response.type)
assertEquals((1L..20L).toList(), response.items.map { it.contentId })
@@ -54,10 +55,11 @@ class ContentOverviewFacadeTest {
eqValue(20L),
eqValue(21),
eqValue(member.id),
eqValue(true)
eqValue(true),
eqValue(Lang.KO)
)
val response = facade.getContents("FIRST_AUDIO_CONTENT", page = 1, size = 20, member = member)
val response = facade.getContents("FIRST_AUDIO_CONTENT", page = 1, size = 20, member = member, lang = Lang.KO)
assertEquals(ContentOverviewType.FIRST_AUDIO_CONTENT, response.type)
assertEquals(listOf(1L, 2L), response.items.map { it.contentId })
@@ -79,6 +81,7 @@ class ContentOverviewFacadeTest {
private fun audioCard(id: Long): AudioCard {
return AudioCard(
creatorMemberId = id + 100L,
audioContentId = id,
title = "audio$id",
duration = "00:01",
@@ -120,6 +120,7 @@ class ContentOverviewPageResponseTest {
private fun audioCard(price: Int, isPointAvailable: Boolean): AudioCard {
return AudioCard(
creatorMemberId = 10L,
audioContentId = price.toLong() + 1,
title = "audio",
duration = "00:01",
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.content.ranking.application
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.v2.content.ranking.application.AudioRankingQueryService
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRanking
@@ -23,6 +24,7 @@ class AudioRankingFacadeTest {
type = AudioRankingType.RISING,
items = listOf(
AudioRankingItem(
creatorMemberId = 10L,
contentId = 1L,
title = "audio",
creatorNickname = "creator",
@@ -33,11 +35,11 @@ class AudioRankingFacadeTest {
)
)
)
Mockito.doReturn(ranking).`when`(queryService).getRankings(AudioRankingType.RISING, member)
Mockito.doReturn(ranking).`when`(queryService).getRankings(AudioRankingType.RISING, member, Lang.KO)
val response = facade.getRankings(AudioRankingType.RISING, member)
val response = facade.getRankings(AudioRankingType.RISING, member, Lang.KO)
Mockito.verify(queryService).getRankings(AudioRankingType.RISING, member)
Mockito.verify(queryService).getRankings(AudioRankingType.RISING, member, Lang.KO)
assertEquals(true, response.showRankChange)
assertEquals(AudioRankingType.RISING, response.type)
assertEquals(1, response.items.size)
@@ -19,6 +19,7 @@ class AudioRankingResponseTest {
type = AudioRankingType.RISING,
items = listOf(
AudioRankingItem(
creatorMemberId = 10L,
contentId = 1001L,
title = "rising audio",
creatorNickname = "creator",
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.`in`.web
import kr.co.vividnext.sodalive.common.CountryContext
import kr.co.vividnext.sodalive.configs.SecurityConfig
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.jwt.JwtAccessDeniedHandler
@@ -12,6 +13,7 @@ import kr.co.vividnext.sodalive.member.MemberAdapter
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.v2.api.content.recommendation.application.AudioRecommendationFacade
import kr.co.vividnext.sodalive.v2.api.content.recommendation.dto.AudioRecommendationsResponse
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
@@ -39,6 +41,11 @@ class AudioRecommendationControllerTest @Autowired constructor(
@MockBean
private lateinit var langContext: LangContext
@BeforeEach
fun setUpLanguage() {
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
}
@MockBean
private lateinit var sodaMessageSource: SodaMessageSource
@@ -54,7 +61,7 @@ class AudioRecommendationControllerTest @Autowired constructor(
@Test
@DisplayName("오디오 추천 조회는 비회원에게 200 OK와 ApiResponse.ok wrapper를 반환한다")
fun shouldReturnRecommendationsForAnonymous() {
Mockito.doReturn(emptyResponse()).`when`(facade).getRecommendations(null)
Mockito.doReturn(emptyResponse()).`when`(facade).getRecommendations(null, Lang.KO)
mockMvc.perform(get("/api/v2/audio/recommendations"))
.andExpect(status().isOk)
@@ -74,13 +81,13 @@ class AudioRecommendationControllerTest @Autowired constructor(
nickname = "viewer",
role = MemberRole.USER
).apply { id = 10L }
Mockito.doReturn(emptyResponse()).`when`(facade).getRecommendations(eqValue(member))
Mockito.doReturn(emptyResponse()).`when`(facade).getRecommendations(eqValue(member), eqValue(Lang.KO))
mockMvc.perform(get("/api/v2/audio/recommendations").with(user(MemberAdapter(member))))
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
Mockito.verify(facade).getRecommendations(eqValue(member))
Mockito.verify(facade).getRecommendations(eqValue(member), eqValue(Lang.KO))
}
private fun emptyResponse(): AudioRecommendationsResponse {
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.api.content.recommendation.application
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.v2.common.domain.RecommendationBanner
import kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryService
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioCard
@@ -20,9 +21,9 @@ class AudioRecommendationFacadeTest {
@Test
@DisplayName("facade는 도메인 추천 결과를 모든 공개 응답 필드로 변환한다")
fun shouldConvertDomainRecommendationsToResponse() {
Mockito.doReturn(domain()).`when`(queryService).getRecommendations(null)
Mockito.doReturn(domain()).`when`(queryService).getRecommendations(null, Lang.KO)
val response = facade.getRecommendations(null)
val response = facade.getRecommendations(null, Lang.KO)
assertEquals(1, response.banners.size)
assertEquals(1, response.originalSeries.size)
@@ -49,6 +50,7 @@ class AudioRecommendationFacadeTest {
private fun domain(): AudioRecommendations {
val card = AudioCard(
creatorMemberId = 10L,
audioContentId = 1L,
title = "audio",
duration = "00:01",
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.audio.adapter.`in`.web
import kr.co.vividnext.sodalive.common.CountryContext
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
@@ -11,6 +12,7 @@ import kr.co.vividnext.sodalive.v2.api.creator.channel.audio.dto.CreatorChannelA
import kr.co.vividnext.sodalive.v2.api.creator.channel.audio.dto.CreatorChannelAudioThemeResponse
import kr.co.vividnext.sodalive.v2.api.creator.channel.common.dto.CreatorChannelAudioContentResponse
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
@@ -47,6 +49,11 @@ class CreatorChannelAudioControllerTest @Autowired constructor(
@MockBean
private lateinit var langContext: LangContext
@BeforeEach
fun setUpLanguage() {
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
}
@MockBean
private lateinit var sodaMessageSource: SodaMessageSource
@@ -88,7 +95,8 @@ class CreatorChannelAudioControllerTest @Autowired constructor(
eqValue(null),
eqValue(null),
eqValue(null),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
mockMvc.perform(
@@ -118,7 +126,8 @@ class CreatorChannelAudioControllerTest @Autowired constructor(
eqValue(null),
eqValue(null),
eqValue(null),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
}
@@ -133,7 +142,8 @@ class CreatorChannelAudioControllerTest @Autowired constructor(
eqValue(999L),
eqValue(-1),
eqValue(100),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
mockMvc.perform(
@@ -158,7 +168,8 @@ class CreatorChannelAudioControllerTest @Autowired constructor(
eqValue(999L),
eqValue(-1),
eqValue(100),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
}
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.audio.application
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
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.v2.common.domain.ContentSort
@@ -33,7 +34,8 @@ class CreatorChannelAudioFacadeTest {
themeId = 10L,
page = 0,
size = 20,
now = now
now = now,
lang = Lang.KO
)
val response = facade.getAudioTab(
@@ -43,7 +45,8 @@ class CreatorChannelAudioFacadeTest {
themeId = 10L,
page = 0,
size = 20,
now = now
now = now,
lang = Lang.KO
)
assertEquals(3, response.audioContentCount)
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.`in`.web
import kr.co.vividnext.sodalive.common.CountryContext
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
@@ -14,6 +15,7 @@ import kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChan
import kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChannelCommunityRepliesResponse
import kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChannelCommunityReplyResponse
import kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChannelCommunityTabResponse
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
@@ -50,6 +52,11 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
@MockBean
private lateinit var langContext: LangContext
@BeforeEach
fun setUpLanguage() {
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
}
@MockBean
private lateinit var sodaMessageSource: SodaMessageSource
@@ -89,7 +96,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer),
eqValue(1),
eqValue(20),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
mockMvc.perform(
@@ -117,7 +125,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer),
eqValue(1),
eqValue(20),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
}
@@ -130,7 +139,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer),
eqValue(-1),
eqValue(100),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
mockMvc.perform(
@@ -148,7 +158,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer),
eqValue(-1),
eqValue(100),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
}
@@ -169,7 +180,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
Mockito.doReturn(createDetailResponse()).`when`(facade).getCommunityPostDetail(
eqValue(101L),
eqValue(viewer),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
mockMvc.perform(
@@ -195,7 +207,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer),
eqValue(0),
eqValue(20),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
mockMvc.perform(
@@ -222,7 +235,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer),
eqValue(-1),
eqValue(100),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
mockMvc.perform(
@@ -240,7 +254,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer),
eqValue(-1),
eqValue(100),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
}
@@ -253,7 +268,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer),
eqValue(0),
eqValue(20),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
mockMvc.perform(
@@ -278,7 +294,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer),
eqValue(-1),
eqValue(100),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
mockMvc.perform(
@@ -296,7 +313,8 @@ class CreatorChannelCommunityControllerTest @Autowired constructor(
eqValue(viewer),
eqValue(-1),
eqValue(100),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
}
@@ -22,6 +22,7 @@ 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.web.servlet.MockMvc
@@ -44,7 +45,8 @@ import javax.persistence.EntityManager
class CreatorChannelCommunityEndToEndTest @Autowired constructor(
private val mockMvc: MockMvc,
private val entityManager: EntityManager,
private val transactionTemplate: TransactionTemplate
private val transactionTemplate: TransactionTemplate,
private val jdbcTemplate: JdbcTemplate
) {
@MockBean
private lateinit var audioContentCloudFront: AudioContentCloudFront
@@ -52,6 +54,67 @@ class CreatorChannelCommunityEndToEndTest @Autowired constructor(
@MockBean
private lateinit var creatorCommunityTranslationService: CreatorCommunityTranslationService
@Test
@DisplayName("커뮤니티 상세의 JA 이름은 크리에이터 댓글과 최신 답글에만 적용하고 USER 이름은 유지한다")
fun shouldLocalizeCreatorAuthorsAndLatestRepliesWithoutChangingUserNames() {
val fixture = transactionTemplate.execute {
val viewer = saveMember("community-localized-viewer", MemberRole.USER)
val creator = saveMember("community-localized-creator", MemberRole.CREATOR)
savePreference(viewer, isAdultContentVisible = true)
val post = saveCommunity(creator, isFixed = false, price = 0, content = "localized-name community")
val userComment = saveCommunityComment(viewer, post)
val creatorComment = saveCommunityComment(creator, post)
saveCommunityComment(creator, post, parent = userComment)
entityManager.flush()
listOf(creator.id!! to "日本語の投稿者", viewer.id!! to "USERには使わない名前").forEach { (memberId, name) ->
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)
""".trimIndent(),
memberId,
name
)
}
updateCommentCreatedAt(userComment.id!!, LocalDateTime.of(2026, 10, 14, 10, 0))
updateCommentCreatedAt(creatorComment.id!!, LocalDateTime.of(2026, 10, 14, 11, 0))
entityManager.clear()
PostFixture(viewer = viewer, postId = post.id!!)
}!!
mockMvc.perform(
get("/api/v2/creator-channels/community-posts/${fixture.postId}")
.header("Accept-Language", "ja")
.with(user(MemberAdapter(fixture.viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.creatorNickname").value("日本語の投稿者"))
.andExpect(jsonPath("$.data.content").value("localized-name community"))
.andExpect(jsonPath("$.data.comments.comments.length()").value(2))
.andExpect(jsonPath("$.data.comments.comments[0].writerNickname").value("日本語の投稿者"))
.andExpect(jsonPath("$.data.comments.comments[1].writerId").value(fixture.viewer.id))
.andExpect(jsonPath("$.data.comments.comments[1].writerNickname").value("community-localized-viewer"))
.andExpect(jsonPath("$.data.comments.comments[1].latestReply.writerNickname").value("日本語の投稿者"))
transactionTemplate.executeWithoutResult {
jdbcTemplate.update(
"update creator_localized_name set display_name = ? where member_id = ? and lang = 'JA'",
"deleted_表示名そのまま",
entityManager.find(CreatorCommunity::class.java, fixture.postId).member!!.id!!
)
}
mockMvc.perform(
get("/api/v2/creator-channels/community-posts/${fixture.postId}")
.header("Accept-Language", "ja")
.with(user(MemberAdapter(fixture.viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.creatorNickname").value("deleted_表示名そのまま"))
.andExpect(jsonPath("$.data.comments.comments[0].writerNickname").value("deleted_表示名そのまま"))
.andExpect(jsonPath("$.data.comments.comments[1].latestReply.writerNickname").value("deleted_表示名そのまま"))
}
@Test
@DisplayName("커뮤니티 탭 API는 E2E로 정렬, fallback, 성인 필터, 유료 미디어 접근 정책을 반환한다")
fun shouldReturnCommunityTabThroughControllerServiceAndRepository() {
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.community.application
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
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.v2.api.creator.channel.community.dto.CreatorChannelCommunityTabResponse
@@ -71,7 +72,8 @@ class CreatorChannelCommunityFacadeTest {
viewer = viewer,
page = -1,
size = 100,
now = now
now = now,
lang = Lang.KO
)
val response = facade.getCommunityTab(
@@ -79,7 +81,8 @@ class CreatorChannelCommunityFacadeTest {
viewer = viewer,
page = -1,
size = 100,
now = now
now = now,
lang = Lang.KO
)
assertEquals(2, response.communityPostCount)
@@ -99,9 +102,9 @@ class CreatorChannelCommunityFacadeTest {
val facade = CreatorChannelCommunityFacade(service)
val viewer = createMember(id = 10L)
val now = LocalDateTime.of(2026, 7, 6, 12, 0)
Mockito.doReturn(createDetail()).`when`(service).getCommunityPostDetail(101L, viewer, now)
Mockito.doReturn(createDetail()).`when`(service).getCommunityPostDetail(101L, viewer, now, Lang.KO)
val response = facade.getCommunityPostDetail(101L, viewer, now)
val response = facade.getCommunityPostDetail(101L, viewer, now, Lang.KO)
assertEquals(101L, response.postId)
assertTrue(response.isLiked)
@@ -119,9 +122,9 @@ class CreatorChannelCommunityFacadeTest {
val facade = CreatorChannelCommunityFacade(service)
val viewer = createMember(id = 10L)
val now = LocalDateTime.of(2026, 7, 6, 12, 0)
Mockito.doReturn(createComments()).`when`(service).getCommunityComments(101L, viewer, -1, 100, now)
Mockito.doReturn(createComments()).`when`(service).getCommunityComments(101L, viewer, -1, 100, now, Lang.KO)
val response = facade.getCommunityComments(101L, viewer, -1, 100, now)
val response = facade.getCommunityComments(101L, viewer, -1, 100, now, Lang.KO)
assertEquals(1, response.commentCount)
assertEquals(0, response.page)
@@ -139,9 +142,9 @@ class CreatorChannelCommunityFacadeTest {
val facade = CreatorChannelCommunityFacade(service)
val viewer = createMember(id = 10L)
val now = LocalDateTime.of(2026, 7, 6, 12, 0)
Mockito.doReturn(createReplies()).`when`(service).getCommunityReplies(201L, viewer, -1, 100, now)
Mockito.doReturn(createReplies()).`when`(service).getCommunityReplies(201L, viewer, -1, 100, now, Lang.KO)
val response = facade.getCommunityReplies(201L, viewer, -1, 100, now)
val response = facade.getCommunityReplies(201L, viewer, -1, 100, now, Lang.KO)
assertEquals(1, response.replyCount)
assertEquals(201L, response.replies.first().commentId)
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.`in`.web
import kr.co.vividnext.sodalive.common.CountryContext
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
@@ -10,6 +11,7 @@ import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application.Crea
import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.dto.CreatorChannelDonationResponse
import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.dto.CreatorChannelDonationTabResponse
import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.dto.MemberDonationRankingResponse
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
@@ -46,6 +48,11 @@ class CreatorChannelDonationControllerTest @Autowired constructor(
@MockBean
private lateinit var langContext: LangContext
@BeforeEach
fun setUpLanguage() {
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
}
@MockBean
private lateinit var sodaMessageSource: SodaMessageSource
@@ -85,7 +92,8 @@ class CreatorChannelDonationControllerTest @Autowired constructor(
eqValue(viewer),
eqValue(1),
eqValue(20),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
mockMvc.perform(
@@ -117,7 +125,8 @@ class CreatorChannelDonationControllerTest @Autowired constructor(
eqValue(viewer),
eqValue(1),
eqValue(20),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
}
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
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.v2.api.creator.channel.donation.dto.CreatorChannelDonationTabResponse
@@ -10,6 +11,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChanne
import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationRanking
import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationTab
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelPage
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.assertTrue
@@ -57,7 +59,10 @@ class CreatorChannelDonationFacadeTest {
@DisplayName("후원 탭 facade는 query service 결과를 공개 응답 DTO로 변환한다")
fun shouldMapDonationTabQueryResultToPublicResponse() {
val service = Mockito.mock(CreatorChannelDonationQueryService::class.java)
val facade = CreatorChannelDonationFacade(service)
val facade = CreatorChannelDonationFacade(
service,
Mockito.mock(CreatorLocalizedNameService::class.java)
)
val viewer = createMember(id = 10L)
val now = LocalDateTime.of(2026, 6, 21, 12, 0)
Mockito.doReturn(createTab()).`when`(service).getDonationTab(
@@ -65,7 +70,8 @@ class CreatorChannelDonationFacadeTest {
viewer = viewer,
page = -1,
size = 100,
now = now
now = now,
lang = Lang.KO
)
val response = facade.getDonationTab(
@@ -73,7 +79,8 @@ class CreatorChannelDonationFacadeTest {
viewer = viewer,
page = -1,
size = 100,
now = now
now = now,
lang = Lang.KO
)
assertEquals(3, response.donationCount)
@@ -106,6 +113,7 @@ class CreatorChannelDonationFacadeTest {
),
donations = listOf(
CreatorChannelDonation(
memberId = 10L,
nickname = "donor",
profileImageUrl = "https://cdn.test/donor.png",
can = 50,
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.adapter.`in`.web
import kr.co.vividnext.sodalive.common.CountryContext
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
@@ -10,6 +11,7 @@ import kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.application.Creat
import kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.dto.CreatorChannelFanTalkReplyResponse
import kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.dto.CreatorChannelFanTalkResponse
import kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.dto.CreatorChannelFanTalkTabResponse
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
@@ -46,6 +48,11 @@ class CreatorChannelFanTalkControllerTest @Autowired constructor(
@MockBean
private lateinit var langContext: LangContext
@BeforeEach
fun setUpLanguage() {
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
}
@MockBean
private lateinit var sodaMessageSource: SodaMessageSource
@@ -85,7 +92,8 @@ class CreatorChannelFanTalkControllerTest @Autowired constructor(
eqValue(viewer),
eqValue(1),
eqValue(20),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
mockMvc.perform(
@@ -111,7 +119,8 @@ class CreatorChannelFanTalkControllerTest @Autowired constructor(
eqValue(viewer),
eqValue(1),
eqValue(20),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
}
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.application
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
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.v2.api.creator.channel.fantalk.dto.CreatorChannelFanTalkTabResponse
@@ -59,7 +60,8 @@ class CreatorChannelFanTalkFacadeTest {
viewer = viewer,
page = -1,
size = 100,
now = now
now = now,
lang = Lang.KO
)
val response = facade.getFanTalkTab(
@@ -67,7 +69,8 @@ class CreatorChannelFanTalkFacadeTest {
viewer = viewer,
page = -1,
size = 100,
now = now
now = now,
lang = Lang.KO
)
assertEquals(2, response.fanTalkCount)
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.home.adapter.`in`.web
import kr.co.vividnext.sodalive.common.CountryContext
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
@@ -21,6 +22,7 @@ import kr.co.vividnext.sodalive.v2.creator.channel.home.domain.CreatorChannelLiv
import kr.co.vividnext.sodalive.v2.creator.channel.home.domain.CreatorChannelSchedule
import kr.co.vividnext.sodalive.v2.creator.channel.home.domain.CreatorChannelSeries
import kr.co.vividnext.sodalive.v2.creator.channel.home.domain.CreatorChannelSns
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
@@ -57,6 +59,11 @@ class CreatorChannelHomeControllerTest @Autowired constructor(
@MockBean
private lateinit var langContext: LangContext
@BeforeEach
fun setUpLanguage() {
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
}
@MockBean
private lateinit var sodaMessageSource: SodaMessageSource
@@ -94,7 +101,8 @@ class CreatorChannelHomeControllerTest @Autowired constructor(
Mockito.doReturn(CreatorChannelHomeResponse.from(createHome())).`when`(facade).getHome(
Mockito.eq(1L),
Mockito.any(Member::class.java) ?: viewer,
Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now()
Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now(),
Mockito.eq(Lang.KO) ?: Lang.KO
)
mockMvc.perform(
@@ -147,7 +155,8 @@ class CreatorChannelHomeControllerTest @Autowired constructor(
Mockito.verify(facade).getHome(
Mockito.eq(1L),
Mockito.eq(viewer) ?: viewer,
Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now()
Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now(),
Mockito.eq(Lang.KO) ?: Lang.KO
)
}
@@ -158,7 +167,8 @@ class CreatorChannelHomeControllerTest @Autowired constructor(
Mockito.doReturn(CreatorChannelHomeResponse.from(createEmptyHome())).`when`(facade).getHome(
Mockito.eq(1L),
Mockito.any(Member::class.java) ?: viewer,
Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now()
Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now(),
Mockito.eq(Lang.KO) ?: Lang.KO
)
mockMvc.perform(
@@ -244,6 +254,7 @@ class CreatorChannelHomeControllerTest @Autowired constructor(
),
channelDonations = listOf(
CreatorChannelDonation(
memberId = 20L,
nickname = "fan",
profileImageUrl = "fan.png",
can = 50,
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.home.application
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.v2.common.domain.CreatorActivityType
@@ -39,13 +40,15 @@ class CreatorChannelHomeFacadeTest {
Mockito.doReturn(createHome()).`when`(service).getHome(
creatorId = 1L,
viewer = viewer,
now = now
now = now,
lang = Lang.KO
)
val response = facade.getHome(
creatorId = 1L,
viewer = viewer,
now = now
now = now,
lang = Lang.KO
)
assertEquals(1L, response.creator.creatorId)
@@ -90,21 +93,23 @@ class CreatorChannelHomeFacadeTest {
Mockito.doReturn(createHome()).`when`(service).getHome(
Mockito.eq(1L),
Mockito.eq(viewer) ?: viewer,
Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now()
Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now(),
Mockito.eq(Lang.KO) ?: Lang.KO
)
try {
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"))
val beforeUtc = LocalDateTime.now(ZoneOffset.UTC).minusSeconds(1)
facade.getHome(creatorId = 1L, viewer = viewer)
facade.getHome(creatorId = 1L, viewer = viewer, lang = Lang.KO)
val afterUtc = LocalDateTime.now(ZoneOffset.UTC).plusSeconds(1)
val nowCaptor = ArgumentCaptor.forClass(LocalDateTime::class.java)
Mockito.verify(service).getHome(
Mockito.eq(1L),
Mockito.eq(viewer) ?: viewer,
nowCaptor.capture() ?: LocalDateTime.MIN
nowCaptor.capture() ?: LocalDateTime.MIN,
Mockito.eq(Lang.KO) ?: Lang.KO
)
val capturedNow = nowCaptor.value
assertTrue(!capturedNow.isBefore(beforeUtc), "captured now should be after UTC lower bound")
@@ -180,6 +185,7 @@ class CreatorChannelHomeFacadeTest {
),
channelDonations = listOf(
CreatorChannelDonation(
memberId = 20L,
nickname = "fan",
profileImageUrl = "fan.png",
can = 50,
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.`in`.web
import kr.co.vividnext.sodalive.common.CountryContext
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
@@ -11,6 +12,7 @@ import kr.co.vividnext.sodalive.v2.api.creator.channel.live.application.CreatorC
import kr.co.vividnext.sodalive.v2.api.creator.channel.live.dto.CreatorChannelLiveResponse
import kr.co.vividnext.sodalive.v2.api.creator.channel.live.dto.CreatorChannelLiveTabResponse
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
@@ -47,6 +49,11 @@ class CreatorChannelLiveControllerTest @Autowired constructor(
@MockBean
private lateinit var langContext: LangContext
@BeforeEach
fun setUpLanguage() {
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
}
@MockBean
private lateinit var sodaMessageSource: SodaMessageSource
@@ -87,7 +94,8 @@ class CreatorChannelLiveControllerTest @Autowired constructor(
eqValue(ContentSort.LATEST),
eqValue(0),
eqValue(20),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
mockMvc.perform(
@@ -112,7 +120,8 @@ class CreatorChannelLiveControllerTest @Autowired constructor(
eqValue(ContentSort.LATEST),
eqValue(0),
eqValue(20),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
}
@@ -127,7 +136,8 @@ class CreatorChannelLiveControllerTest @Autowired constructor(
eqValue(ContentSort.LATEST),
eqValue(0),
eqValue(20),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
mockMvc.perform(
@@ -162,7 +172,8 @@ class CreatorChannelLiveControllerTest @Autowired constructor(
eqValue(ContentSort.LATEST),
eqValue(-1),
eqValue(20),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
mockMvc.perform(
@@ -185,7 +196,8 @@ class CreatorChannelLiveControllerTest @Autowired constructor(
eqValue(ContentSort.LATEST),
eqValue(0),
eqValue(0),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
mockMvc.perform(
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.live.application
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.v2.common.domain.ContentSort
@@ -30,7 +31,8 @@ class CreatorChannelLiveFacadeTest {
sort = ContentSort.LATEST,
page = 0,
size = 20,
now = now
now = now,
lang = Lang.KO
)
val response = facade.getLiveTab(
@@ -39,7 +41,8 @@ class CreatorChannelLiveFacadeTest {
sort = ContentSort.LATEST,
page = 0,
size = 20,
now = now
now = now,
lang = Lang.KO
)
assertEquals(1, response.liveReplayContentCount)
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.series.adapter.`in`.web
import kr.co.vividnext.sodalive.common.CountryContext
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
@@ -10,6 +11,7 @@ import kr.co.vividnext.sodalive.v2.api.creator.channel.series.application.Creato
import kr.co.vividnext.sodalive.v2.api.creator.channel.series.dto.CreatorChannelSeriesResponse
import kr.co.vividnext.sodalive.v2.api.creator.channel.series.dto.CreatorChannelSeriesTabResponse
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
@@ -46,6 +48,11 @@ class CreatorChannelSeriesControllerTest @Autowired constructor(
@MockBean
private lateinit var langContext: LangContext
@BeforeEach
fun setUpLanguage() {
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
}
@MockBean
private lateinit var sodaMessageSource: SodaMessageSource
@@ -86,7 +93,8 @@ class CreatorChannelSeriesControllerTest @Autowired constructor(
eqValue("POPULAR"),
eqValue(1),
eqValue(20),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
mockMvc.perform(
@@ -118,7 +126,8 @@ class CreatorChannelSeriesControllerTest @Autowired constructor(
eqValue("POPULAR"),
eqValue(1),
eqValue(20),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
}
@@ -132,7 +141,8 @@ class CreatorChannelSeriesControllerTest @Autowired constructor(
eqValue("INVALID"),
eqValue(-1),
eqValue(100),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
mockMvc.perform(
@@ -153,7 +163,8 @@ class CreatorChannelSeriesControllerTest @Autowired constructor(
eqValue("INVALID"),
eqValue(-1),
eqValue(100),
anyValue(LocalDateTime.now())
anyValue(LocalDateTime.now()),
eqValue(Lang.KO)
)
}
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.creator.channel.series.application
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
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.v2.api.creator.channel.series.dto.CreatorChannelSeriesTabResponse
@@ -63,7 +64,8 @@ class CreatorChannelSeriesFacadeTest {
sort = "OWNED",
page = 1,
size = 20,
now = now
now = now,
lang = Lang.KO
)
val response = facade.getSeriesTab(
@@ -72,7 +74,8 @@ class CreatorChannelSeriesFacadeTest {
sort = "OWNED",
page = 1,
size = 20,
now = now
now = now,
lang = Lang.KO
)
assertEquals(2, response.seriesCount)
@@ -9,11 +9,13 @@ import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
import kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.CreatorRankingSnapshot
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScoreSpec
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType
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.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
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.web.servlet.MockMvc
@@ -37,8 +39,51 @@ import javax.persistence.EntityManager
class CreatorRankingControllerTest @Autowired constructor(
private val mockMvc: MockMvc,
private val memberRepository: MemberRepository,
private val entityManager: EntityManager
private val entityManager: EntityManager,
private val jdbcTemplate: JdbcTemplate
) {
@Test
@DisplayName("언어 중립 랭킹은 조회 언어의 현재 이름을 반환하고 저장된 스냅샷 이름은 변경하지 않는다")
fun shouldRenderCurrentLocalizedNamesWithoutRewritingRankingSnapshots() {
val localized = saveMember("ranking-localized-before", MemberRole.CREATOR)
val fallback = saveMember("ranking-fallback-before", MemberRole.CREATOR)
saveSnapshot(localized.id!!, "ranking-localized-before", null, rankNo = 1)
saveSnapshot(fallback.id!!, "ranking-fallback-before", null, rankNo = 2)
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)
""".trimIndent(),
localized.id,
"日本語ランキング名"
)
localized.nickname = "ranking-localized-current"
fallback.nickname = "ranking-fallback-current"
entityManager.flush()
entityManager.clear()
mockMvc.perform(get("/api/v2/home/rankings/creators").header("Accept-Language", "ja"))
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.items[0].creatorId").value(localized.id))
.andExpect(jsonPath("$.data.items[0].nickname").value("日本語ランキング名"))
.andExpect(jsonPath("$.data.items[1].creatorId").value(fallback.id))
.andExpect(jsonPath("$.data.items[1].nickname").value("ranking-fallback-current"))
mockMvc.perform(get("/api/v2/home/rankings/creators").header("Accept-Language", "ko"))
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.items[0].nickname").value("ranking-localized-current"))
.andExpect(jsonPath("$.data.items[1].nickname").value("ranking-fallback-current"))
assertEquals(
listOf("ranking-localized-before", "ranking-fallback-before"),
entityManager.createQuery(
"select s.nickname from CreatorRankingSnapshot s order by s.rankNo",
String::class.java
).resultList
)
assertEquals("ranking-localized-current", memberRepository.findById(localized.id!!).get().nickname)
}
@Test
@DisplayName("크리에이터 랭킹 API는 공개 응답 스키마만 노출하고 내부 스냅샷 필드를 숨긴다")
fun shouldReturnOnlyPublicCreatorRankingSchemaThroughEndpoint() {
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.home
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.live.room.LiveRoom
import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.member.Member
@@ -35,6 +36,7 @@ import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.boot.test.system.CapturedOutput
import org.springframework.boot.test.system.OutputCaptureExtension
import org.springframework.http.MediaType
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.web.servlet.MockMvc
@@ -56,11 +58,50 @@ class HomeRecommendationControllerTest @Autowired constructor(
private val mockMvc: MockMvc,
private val memberRepository: MemberRepository,
private val creatorFollowingRepository: CreatorFollowingRepository,
private val entityManager: EntityManager
private val entityManager: EntityManager,
private val jdbcTemplate: JdbcTemplate
) {
@MockBean
private lateinit var creatorCommunityTranslationService: CreatorCommunityTranslationService
@Test
@DisplayName("native 데뷔 크리에이터 조회도 JA 별칭을 우선하고 KO 별칭 미등록 시 현재 원본 이름을 반환한다")
fun shouldLocalizeNativeDebutCreatorResultsByRequestLanguage() {
val viewer = saveMember("localized-native-viewer", MemberRole.USER)
val creator = saveMember("localized-native-before", MemberRole.CREATOR)
saveLiveRoom(creator, LocalDateTime.now().minusDays(1), "localized-native-live", isAdult = false)
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)
""".trimIndent(),
creator.id,
"日本語デビュー名"
)
creator.nickname = "localized-native-current"
entityManager.flush()
entityManager.clear()
mockMvc.perform(
get("/api/v2/home/recommendations/debut-creators")
.header("Accept-Language", "ko")
.with(user(MemberAdapter(viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.items[0].creatorId").value(creator.id))
.andExpect(jsonPath("$.data.items[0].creatorNickname").value("localized-native-current"))
mockMvc.perform(
get("/api/v2/home/recommendations/debut-creators")
.header("Accept-Language", "ja")
.with(user(MemberAdapter(viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.items[0].creatorId").value(creator.id))
.andExpect(jsonPath("$.data.items[0].creatorNickname").value("日本語デビュー名"))
}
@Test
@DisplayName("추천 크리에이터 동시 팔로우 비로그인 요청은 Spring Security에서 거부한다")
fun shouldRejectAnonymousFollowRequest() {
@@ -328,12 +369,16 @@ class HomeRecommendationControllerTest @Autowired constructor(
fun shouldLogHomeRecommendationFailure(output: CapturedOutput) {
val failingQueryService = Mockito.mock(HomeRecommendationQueryService::class.java)
val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
val facade = HomeRecommendationFacade(failingQueryService, preferenceService, "https://cdn.test")
Mockito.`when`(failingQueryService.findLiveRecommendations(limit = 20, memberId = null, includeAdultLives = false))
val facade = HomeRecommendationFacade(
failingQueryService,
preferenceService,
"https://cdn.test"
)
Mockito.`when`(failingQueryService.findLiveRecommendations(limit = 20, memberId = null, lang = Lang.KO))
.thenThrow(IllegalStateException("home query failed"))
val exception = assertThrows(IllegalStateException::class.java) {
facade.getHomeRecommendations(member = null)
facade.getHomeRecommendations(member = null, lang = Lang.KO)
}
assertEquals("home query failed", exception.message)
@@ -366,7 +411,11 @@ class HomeRecommendationControllerTest @Autowired constructor(
val member = saveMember("page-failure-viewer", MemberRole.USER)
val failingQueryService = Mockito.mock(HomeRecommendationQueryService::class.java)
val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
val facade = HomeRecommendationFacade(failingQueryService, preferenceService, "https://cdn.test")
val facade = HomeRecommendationFacade(
failingQueryService,
preferenceService,
"https://cdn.test"
)
Mockito.`when`(preferenceService.canViewAdultContent(member)).thenReturn(false)
Mockito.`when`(
failingQueryService.findLiveRecommendations(
@@ -375,13 +424,14 @@ class HomeRecommendationControllerTest @Autowired constructor(
memberId = member.id,
includeAdultLives = false,
isViewerCreator = false,
effectiveViewerGender = Gender.NONE
effectiveViewerGender = Gender.NONE,
lang = Lang.KO
)
)
.thenThrow(IllegalStateException("page query failed"))
val exception = assertThrows(IllegalStateException::class.java) {
facade.getLives(member, page = 0, size = 20)
facade.getLives(member, page = 0, size = 20, lang = Lang.KO)
}
assertEquals("page query failed", exception.message)
@@ -395,7 +445,11 @@ class HomeRecommendationControllerTest @Autowired constructor(
val member = saveMember("other-page-failure-viewer", MemberRole.USER)
val failingQueryService = Mockito.mock(HomeRecommendationQueryService::class.java)
val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
val facade = HomeRecommendationFacade(failingQueryService, preferenceService, "https://cdn.test")
val facade = HomeRecommendationFacade(
failingQueryService,
preferenceService,
"https://cdn.test"
)
Mockito.`when`(preferenceService.canViewAdultContent(member)).thenReturn(false)
Mockito.`when`(
failingQueryService.findRecentDebutCreators(
@@ -403,13 +457,16 @@ class HomeRecommendationControllerTest @Autowired constructor(
offset = Mockito.eq(0L),
limit = Mockito.eq(21),
memberId = Mockito.eq(member.id),
includeAdultContents = Mockito.eq(false)
includeAdultContents = Mockito.eq(false),
lang = Mockito.eq(Lang.KO) ?: Lang.KO
)
).thenThrow(IllegalStateException("debut page failed"))
Mockito.`when`(failingQueryService.findAiCharacterRecommendations(offset = 0L, limit = 21))
.thenThrow(IllegalStateException("ai page failed"))
assertThrows(IllegalStateException::class.java) { facade.getRecentDebutCreators(member, page = 0, size = 20) }
assertThrows(IllegalStateException::class.java) {
facade.getRecentDebutCreators(member, page = 0, size = 20, lang = Lang.KO)
}
assertThrows(IllegalStateException::class.java) { facade.getAiCharacters(member, page = 0, size = 20) }
assertTrue(output.out.contains("section=DEBUT_CREATOR"))
@@ -1,7 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.home.application
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
@@ -35,11 +34,14 @@ class HomeRecommendationFacadeTest {
private val queryService = HomeRecommendationQueryService(
queryPort,
EmptyRecommendationSnapshotPort(),
langContext = LangContext(),
creatorCommunityTranslationService = creatorCommunityTranslationService
)
private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
private val facade = HomeRecommendationFacade(queryService, preferenceService, "https://cdn.test")
private val facade = HomeRecommendationFacade(
queryService,
preferenceService,
"https://cdn.test"
)
@Test
@DisplayName("홈 첫 오디오 응답은 무료 콘텐츠의 포인트 가능 여부를 false로 보정한다")
@@ -50,7 +52,7 @@ class HomeRecommendationFacadeTest {
firstAudio(price = 100, isPointAvailable = false)
)
val response = facade.getHomeRecommendations(null)
val response = facade.getHomeRecommendations(null, Lang.KO)
assertFalse(response.firstAudioContents[0].isPointAvailable)
assertTrue(response.firstAudioContents[1].isPointAvailable)
@@ -63,7 +65,7 @@ class HomeRecommendationFacadeTest {
val member = Member(password = "password", nickname = "member").apply { id = 10L }
Mockito.doReturn(true).`when`(preferenceService).canViewAdultContent(member)
facade.getHomeRecommendations(member)
facade.getHomeRecommendations(member, Lang.KO)
assertTrue(queryPort.bannerIncludeAdultBanners)
}
@@ -74,7 +76,7 @@ class HomeRecommendationFacadeTest {
val member = Member(password = "password", nickname = "jp-member").apply { id = 11L }
Mockito.doReturn("JP").`when`(preferenceService).resolveCountryCode(member)
facade.getHomeRecommendations(member)
facade.getHomeRecommendations(member, Lang.KO)
assertTrue(queryPort.bannerLang == Lang.JA)
}
@@ -84,7 +86,7 @@ class HomeRecommendationFacadeTest {
fun shouldPassKoToHomeBannerQueryForNonJapan() {
Mockito.doReturn("KR").`when`(preferenceService).resolveCountryCode(null)
facade.getHomeRecommendations(null)
facade.getHomeRecommendations(null, Lang.KO)
assertTrue(queryPort.bannerLang == Lang.KO)
}
@@ -94,7 +96,7 @@ class HomeRecommendationFacadeTest {
fun shouldPassCreatorRoleAndAuthGenderToHomeLiveQuery() {
val member = createMember(id = 12L, role = MemberRole.CREATOR, gender = Gender.FEMALE, authGender = 1)
facade.getHomeRecommendations(member)
facade.getHomeRecommendations(member, Lang.KO)
assertTrue(queryPort.liveIsViewerCreator == true)
assertTrue(queryPort.liveEffectiveViewerGender == Gender.MALE)
@@ -105,7 +107,7 @@ class HomeRecommendationFacadeTest {
fun shouldPassCreatorRoleAndMemberGenderToLivePageQuery() {
val member = createMember(id = 13L, role = MemberRole.CREATOR, gender = Gender.FEMALE)
facade.getLives(member, page = 0, size = 20)
facade.getLives(member, page = 0, size = 20, lang = Lang.KO)
assertTrue(queryPort.liveIsViewerCreator == true)
assertTrue(queryPort.liveEffectiveViewerGender == Gender.FEMALE)
@@ -153,7 +155,8 @@ class HomeRecommendationFacadeTest {
memberId: Long?,
includeAdultLives: Boolean,
isViewerCreator: Boolean,
effectiveViewerGender: Gender?
effectiveViewerGender: Gender?,
lang: Lang
): List<HomeLiveRecommendationRecord> {
liveIsViewerCreator = isViewerCreator
liveEffectiveViewerGender = effectiveViewerGender
@@ -174,7 +177,8 @@ class HomeRecommendationFacadeTest {
override fun findRecentlyActiveCreators(
limit: Int,
memberId: Long?,
includeAdultActivities: Boolean
includeAdultActivities: Boolean,
lang: Lang
): List<RecentlyActiveCreatorRecord> = emptyList()
override fun findRecentDebutCreators(
@@ -182,7 +186,8 @@ class HomeRecommendationFacadeTest {
offset: Long,
limit: Int,
memberId: Long?,
includeAdultContents: Boolean
includeAdultContents: Boolean,
lang: Lang
): List<RecentDebutCreatorRecord> = emptyList()
override fun findFirstAudioContents(
@@ -220,20 +225,23 @@ class HomeRecommendationFacadeTest {
override fun findCheerCreatorRecommendationDetails(
creatorIds: List<Long>,
memberId: Long?
memberId: Long?,
lang: Lang
): List<HomeCheerCreatorRecommendationRecord> = emptyList()
override fun findPopularCommunityRecommendationDetails(
communityIds: List<Long>,
memberId: Long?,
includeAdultCommunities: Boolean
includeAdultCommunities: Boolean,
lang: Lang
): List<HomePopularCommunityRecommendationRecord> = emptyList()
override fun findGenreCreatorRecommendations(
memberId: Long?,
includeAdultGenres: Boolean,
genreLimit: Int,
creatorLimit: Int
creatorLimit: Int,
lang: Lang
): List<HomeGenreCreatorRecommendationGroup> = emptyList()
}
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.home.following.adapter.`in`.web
import kr.co.vividnext.sodalive.common.CountryContext
import kr.co.vividnext.sodalive.configs.SecurityConfig
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.jwt.JwtAccessDeniedHandler
@@ -12,6 +13,7 @@ import kr.co.vividnext.sodalive.member.MemberAdapter
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.v2.api.home.following.application.HomeFollowingFacade
import kr.co.vividnext.sodalive.v2.api.home.following.dto.HomeFollowingTabResponse
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
@@ -39,6 +41,11 @@ class HomeFollowingControllerTest @Autowired constructor(
@MockBean
private lateinit var langContext: LangContext
@BeforeEach
fun setUpLanguage() {
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
}
@MockBean
private lateinit var sodaMessageSource: SodaMessageSource
@@ -54,7 +61,7 @@ class HomeFollowingControllerTest @Autowired constructor(
@Test
@DisplayName("팔로잉 탭 조회는 비회원에게 200 OK와 로그인 필요 응답을 반환한다")
fun shouldReturnLoginRequiredForAnonymous() {
Mockito.doReturn(HomeFollowingTabResponse.loginRequired()).`when`(facade).getFollowingTab(null)
Mockito.doReturn(HomeFollowingTabResponse.loginRequired()).`when`(facade).getFollowingTab(null, Lang.KO)
mockMvc.perform(get("/api/v2/home/following"))
.andExpect(status().isOk)
@@ -76,14 +83,14 @@ class HomeFollowingControllerTest @Autowired constructor(
nickname = "viewer",
role = MemberRole.USER
).apply { id = 10L }
Mockito.doReturn(loggedInEmptyResponse()).`when`(facade).getFollowingTab(eqValue(member))
Mockito.doReturn(loggedInEmptyResponse()).`when`(facade).getFollowingTab(eqValue(member), eqValue(Lang.KO))
mockMvc.perform(get("/api/v2/home/following").with(user(MemberAdapter(member))))
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.isLoginRequired").value(false))
Mockito.verify(facade).getFollowingTab(eqValue(member))
Mockito.verify(facade).getFollowingTab(eqValue(member), eqValue(Lang.KO))
}
private fun loggedInEmptyResponse(): HomeFollowingTabResponse {
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.home.following.application
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.v2.chat.dto.ChatRoomListItemResponse
@@ -24,12 +25,15 @@ import org.mockito.Mockito
class HomeFollowingFacadeTest {
private val queryService = Mockito.mock(HomeFollowingQueryService::class.java)
private val chatRoomListService = Mockito.mock(ChatRoomListService::class.java)
private val facade = HomeFollowingFacade(queryService, chatRoomListService)
private val facade = HomeFollowingFacade(
queryService,
chatRoomListService
)
@Test
@DisplayName("비로그인 회원은 로그인 필요 응답을 반환하고 조회/채팅 서비스를 호출하지 않는다")
fun shouldReturnLoginRequiredWithoutCallingServicesForAnonymous() {
val response = facade.getFollowingTab(null)
val response = facade.getFollowingTab(null, Lang.KO)
assertTrue(response.isLoginRequired)
assertTrue(response.followingCreators.isEmpty())
@@ -58,12 +62,12 @@ class HomeFollowingFacadeTest {
lastMessage = "hello",
lastMessageAt = "2026-06-25T01:00:00Z"
)
Mockito.doReturn(home).`when`(queryService).findHomeFollowing(member)
Mockito.doReturn(home).`when`(queryService).findHomeFollowing(member, Lang.KO)
Mockito.doReturn(ChatRoomListPageResponse(rooms = listOf(recentChat), hasMore = false, nextCursor = null))
.`when`(chatRoomListService)
.getRooms(member, filter = "ALL", cursor = null, limit = 10, followedCreatorsOnly = true)
.getRooms(member, Lang.KO, filter = "ALL", cursor = null, limit = 10, followedCreatorsOnly = true)
val response = facade.getFollowingTab(member)
val response = facade.getFollowingTab(member, Lang.KO)
assertFalse(response.isLoginRequired)
assertEquals(1L, response.followingCreators.single().creatorId)
@@ -71,9 +75,9 @@ class HomeFollowingFacadeTest {
assertEquals(listOf(recentChat), response.recentChats)
assertEquals("LIVE:4", response.monthlySchedules.single().scheduleId)
assertEquals("news-5", response.recentNews.single().newsId)
Mockito.verify(queryService).findHomeFollowing(member)
Mockito.verify(queryService).findHomeFollowing(member, Lang.KO)
Mockito.verify(chatRoomListService)
.getRooms(member, filter = "ALL", cursor = null, limit = 10, followedCreatorsOnly = true)
.getRooms(member, Lang.KO, filter = "ALL", cursor = null, limit = 10, followedCreatorsOnly = true)
}
private fun homeFollowing(): HomeFollowing {
@@ -87,6 +91,7 @@ class HomeFollowingFacadeTest {
),
onAirLives = listOf(
HomeFollowingLive(
creatorMemberId = 1L,
liveId = 2L,
creatorProfileImageUrl = "https://cdn.test/live.png",
creatorNickname = "creator",
@@ -110,6 +115,7 @@ class HomeFollowingFacadeTest {
),
recentNews = listOf(
HomeFollowingNews(
creatorMemberId = 1L,
newsId = "news-5",
type = FollowingNewsType.CREATOR_RANKING,
visibleFromAtUtc = "2026-06-25T04:00:00Z",
@@ -115,6 +115,7 @@ class HomeFollowingTabResponseTest {
followingCreators = listOf(HomeFollowingCreator(1L, "creator", "https://cdn/profile.jpg")),
onAirLives = listOf(
HomeFollowingLive(
creatorMemberId = 101L,
liveId = 10L,
creatorProfileImageUrl = "https://cdn/live-profile.jpg",
creatorNickname = "live-creator",
@@ -147,6 +148,7 @@ class HomeFollowingTabResponseTest {
),
recentNews = listOf(
HomeFollowingNews(
creatorMemberId = 1L,
newsId = "30",
type = FollowingNewsType.CREATOR_RANKING,
visibleFromAtUtc = "2026-06-25T09:00:00Z",
@@ -158,6 +160,7 @@ class HomeFollowingTabResponseTest {
)
),
HomeFollowingNews(
creatorMemberId = 2L,
newsId = "31",
type = FollowingNewsType.AUDIO_CONTENT,
visibleFromAtUtc = "2026-06-26T00:00:00Z",
@@ -170,6 +173,7 @@ class HomeFollowingTabResponseTest {
)
),
HomeFollowingNews(
creatorMemberId = 3L,
newsId = "32",
type = FollowingNewsType.PHOTO_CONTENT,
visibleFromAtUtc = "2026-06-27T00:00:00Z",
@@ -182,6 +186,7 @@ class HomeFollowingTabResponseTest {
)
),
HomeFollowingNews(
creatorMemberId = 4L,
newsId = "33",
type = FollowingNewsType.CONTENT_RANKING,
visibleFromAtUtc = "2026-06-28T00:00:00Z",
@@ -193,6 +198,7 @@ class HomeFollowingTabResponseTest {
)
),
HomeFollowingNews(
creatorMemberId = 5L,
newsId = "34",
type = FollowingNewsType.COMMUNITY_POST,
visibleFromAtUtc = "2026-06-25T03:00:00Z",
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.home.live.adapter.`in`.web
import kr.co.vividnext.sodalive.common.CountryContext
import kr.co.vividnext.sodalive.configs.SecurityConfig
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.jwt.JwtAccessDeniedHandler
@@ -13,6 +14,7 @@ import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.v2.api.home.live.application.HomeOnAirLiveFacade
import kr.co.vividnext.sodalive.v2.api.home.live.dto.HomeOnAirLivePageResponse
import kr.co.vividnext.sodalive.v2.api.home.live.dto.HomeOnAirLiveResponse
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
@@ -41,6 +43,11 @@ class HomeOnAirLiveControllerTest @Autowired constructor(
@MockBean
private lateinit var langContext: LangContext
@BeforeEach
fun setUpLanguage() {
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
}
@MockBean
private lateinit var sodaMessageSource: SodaMessageSource
@@ -61,7 +68,7 @@ class HomeOnAirLiveControllerTest @Autowired constructor(
@DisplayName("현재 진행 중인 라이브 조회는 인증 회원과 page를 facade에 전달하고 성공 응답을 반환한다")
fun shouldPassAuthenticatedMemberAndPageToFacade() {
val member = createMember(100L)
Mockito.doReturn(createResponse()).`when`(facade).getOnAirLives(eqValue(member), eqValue(2))
Mockito.doReturn(createResponse()).`when`(facade).getOnAirLives(eqValue(member), eqValue(2), eqValue(Lang.KO))
mockMvc.perform(
get("/api/v2/home/on-air-lives")
@@ -73,7 +80,7 @@ class HomeOnAirLiveControllerTest @Autowired constructor(
.andExpect(jsonPath("$.data.size").value(20))
.andExpect(jsonPath("$.data.items[0].title").value("paid live"))
Mockito.verify(facade).getOnAirLives(eqValue(member), eqValue(2))
Mockito.verify(facade).getOnAirLives(eqValue(member), eqValue(2), eqValue(Lang.KO))
}
private fun createResponse() = HomeOnAirLivePageResponse(
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.home.live.application
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
@@ -16,7 +17,11 @@ import java.time.LocalDateTime
class HomeOnAirLiveFacadeTest {
private val queryService = Mockito.mock(HomeRecommendationQueryService::class.java)
private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
private val facade = HomeOnAirLiveFacade(queryService, preferenceService, "https://cdn.test")
private val facade = HomeOnAirLiveFacade(
queryService,
preferenceService,
"https://cdn.test"
)
@Test
@DisplayName("현재 진행 중인 라이브 facade는 20개 고정 page와 hasNext를 조립한다")
@@ -29,10 +34,11 @@ class HomeOnAirLiveFacadeTest {
eqValue(member.id),
eqValue(true),
eqValue(false),
eqValue(Gender.NONE)
eqValue(Gender.NONE),
eqValue(Lang.KO)
)
val response = facade.getOnAirLives(member, page = 0)
val response = facade.getOnAirLives(member, page = 0, lang = Lang.KO)
assertEquals(0, response.page)
assertEquals(20, response.size)
@@ -44,7 +50,8 @@ class HomeOnAirLiveFacadeTest {
eqValue(member.id),
eqValue(true),
eqValue(false),
eqValue(Gender.NONE)
eqValue(Gender.NONE),
eqValue(Lang.KO)
)
}
@@ -59,10 +66,11 @@ class HomeOnAirLiveFacadeTest {
eqValue(member.id),
eqValue(false),
eqValue(false),
eqValue(Gender.NONE)
eqValue(Gender.NONE),
eqValue(Lang.KO)
)
val response = facade.getOnAirLives(member, page = 0)
val response = facade.getOnAirLives(member, page = 0, lang = Lang.KO)
assertEquals("https://cdn.test/profile/default-profile.png", response.items.single().creatorProfileImage)
}
@@ -79,10 +87,11 @@ class HomeOnAirLiveFacadeTest {
eqValue(member.id),
eqValue(false),
eqValue(false),
eqValue(Gender.NONE)
eqValue(Gender.NONE),
eqValue(Lang.KO)
)
val response = facade.getOnAirLives(member, page = 0)
val response = facade.getOnAirLives(member, page = 0, lang = Lang.KO)
assertEquals("2026-06-26T12:30:00Z", response.items.single().beginDateTimeUtc)
}
@@ -98,10 +107,11 @@ class HomeOnAirLiveFacadeTest {
eqValue(member.id),
eqValue(true),
eqValue(true),
eqValue(Gender.MALE)
eqValue(Gender.MALE),
eqValue(Lang.KO)
)
facade.getOnAirLives(member, page = 0)
facade.getOnAirLives(member, page = 0, lang = Lang.KO)
Mockito.verify(queryService).findLiveRecommendations(
eqValue(0L),
@@ -109,7 +119,8 @@ class HomeOnAirLiveFacadeTest {
eqValue(member.id),
eqValue(true),
eqValue(true),
eqValue(Gender.MALE)
eqValue(Gender.MALE),
eqValue(Lang.KO)
)
}
@@ -118,6 +129,7 @@ class HomeOnAirLiveFacadeTest {
creatorProfileImage: String? = "profile.png",
beginDateTime: LocalDateTime = LocalDateTime.of(2026, 6, 26, 12, 30)
) = HomeLiveRecommendationRecord(
creatorMemberId = id + 100L,
liveRoomId = id,
creatorNickname = "creator-$id",
creatorProfileImage = creatorProfileImage,
@@ -1,5 +1,7 @@
package kr.co.vividnext.sodalive.v2.chat
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.v2.chat.controller.ChatRoomListController
import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListItemResponse
@@ -21,7 +23,7 @@ class ChatRoomListControllerTest {
@BeforeEach
fun setUp() {
service = Mockito.mock(ChatRoomListService::class.java)
controller = ChatRoomListController(service)
controller = ChatRoomListController(service, LangContext())
}
@Test
@@ -45,12 +47,12 @@ class ChatRoomListControllerTest {
hasMore = false,
nextCursor = null
)
Mockito.`when`(service.getRooms(member, "DM", "cursor", 10)).thenReturn(serviceResponse)
Mockito.`when`(service.getRooms(member, Lang.KO, "DM", "cursor", 10)).thenReturn(serviceResponse)
val response = controller.getRooms(member = member, filter = "DM", cursor = "cursor", limit = 10)
assertTrue(response.success)
assertEquals(serviceResponse, response.data)
Mockito.verify(service).getRooms(member, "DM", "cursor", 10)
Mockito.verify(service).getRooms(member, Lang.KO, "DM", "cursor", 10)
}
}
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.chat
import kr.co.vividnext.sodalive.chat.room.ChatMessageType
import kr.co.vividnext.sodalive.chat.room.repository.ChatRoomRepository
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListQueryDto
import kr.co.vividnext.sodalive.v2.chat.service.ChatRoomListService
@@ -55,7 +56,7 @@ class ChatRoomListServiceTest {
)
)
Mockito.`when`(
dmRoomRepository.findDmChatListRooms(1L, null, null, null, false, PageRequest.of(0, 31))
dmRoomRepository.findDmChatListRooms(1L, null, null, null, false, Lang.KO, PageRequest.of(0, 31))
).thenReturn(
listOf(
ChatRoomListQueryDto(
@@ -70,7 +71,7 @@ class ChatRoomListServiceTest {
)
)
val response = service.getRooms(member, filter = "ALL", cursor = null, limit = 30)
val response = service.getRooms(member, Lang.KO, filter = "ALL", cursor = null, limit = 30)
assertFalse(response.hasMore)
assertEquals(listOf("DM", "AI"), response.rooms.map { it.chatType })
@@ -85,7 +86,7 @@ class ChatRoomListServiceTest {
fun shouldReturnOnlyDmRoomsWithVoicePreview() {
val member = member(1L)
Mockito.`when`(
dmRoomRepository.findDmChatListRooms(1L, null, null, null, false, PageRequest.of(0, 31))
dmRoomRepository.findDmChatListRooms(1L, null, null, null, false, Lang.KO, PageRequest.of(0, 31))
).thenReturn(
listOf(
ChatRoomListQueryDto(
@@ -100,7 +101,7 @@ class ChatRoomListServiceTest {
)
)
val response = service.getRooms(member, filter = "DM", cursor = null, limit = 30)
val response = service.getRooms(member, Lang.KO, filter = "DM", cursor = null, limit = 30)
assertEquals(1, response.rooms.size)
assertEquals("DM", response.rooms[0].chatType)
@@ -128,7 +129,7 @@ class ChatRoomListServiceTest {
aiRoomRepository.findAiChatListRooms(1L, null, null, null, false, PageRequest.of(0, 31))
).thenReturn(rows)
val response = service.getRooms(member, filter = "AI", cursor = null, limit = 30)
val response = service.getRooms(member, Lang.KO, filter = "AI", cursor = null, limit = 30)
assertEquals(30, response.rooms.size)
assertTrue(response.hasMore)
@@ -157,7 +158,7 @@ class ChatRoomListServiceTest {
)
)
Mockito.`when`(
dmRoomRepository.findDmChatListRooms(1L, cursorAt, "DM", 20L, false, PageRequest.of(0, 31))
dmRoomRepository.findDmChatListRooms(1L, cursorAt, "DM", 20L, false, Lang.KO, PageRequest.of(0, 31))
).thenReturn(
listOf(
ChatRoomListQueryDto(
@@ -181,7 +182,7 @@ class ChatRoomListServiceTest {
)
)
val response = service.getRooms(member, filter = "ALL", cursor = "2026-05-14T12:00:00:DM:20", limit = 30)
val response = service.getRooms(member, Lang.KO, filter = "ALL", cursor = "2026-05-14T12:00:00:DM:20", limit = 30)
assertEquals(listOf(30L, 10L), response.rooms.map { it.roomId })
assertEquals("https://cdn.test/profile/default-profile.png", response.rooms[0].targetImageUrl)
@@ -194,13 +195,13 @@ class ChatRoomListServiceTest {
val pageable = PageRequest.of(0, 11)
Mockito.`when`(aiRoomRepository.findAiChatListRooms(1L, null, null, null, true, pageable))
.thenReturn(emptyList())
Mockito.`when`(dmRoomRepository.findDmChatListRooms(1L, null, null, null, true, pageable))
Mockito.`when`(dmRoomRepository.findDmChatListRooms(1L, null, null, null, true, Lang.KO, pageable))
.thenReturn(emptyList())
service.getRooms(member, filter = "ALL", cursor = null, limit = 10, followedCreatorsOnly = true)
service.getRooms(member, Lang.KO, filter = "ALL", cursor = null, limit = 10, followedCreatorsOnly = true)
Mockito.verify(aiRoomRepository).findAiChatListRooms(1L, null, null, null, true, pageable)
Mockito.verify(dmRoomRepository).findDmChatListRooms(1L, null, null, null, true, pageable)
Mockito.verify(dmRoomRepository).findDmChatListRooms(1L, null, null, null, true, Lang.KO, pageable)
}
private fun member(id: Long) = Member(password = "pw", nickname = "user").apply { this.id = id }
@@ -2,7 +2,6 @@ package kr.co.vividnext.sodalive.v2.content.all.application
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
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.MemberRole
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
@@ -27,7 +26,15 @@ class MainContentAllQueryServiceTest {
val port = FakeMainContentAllQueryPort()
val service = createService(port)
val tab = service.getContents(type = "AUDIO", sort = "POPULAR", dayOfWeek = "MON", page = 1, size = 20, member = null)
val tab = service.getContents(
type = "AUDIO",
sort = "POPULAR",
dayOfWeek = "MON",
page = 1,
size = 20,
member = null,
lang = Lang.EN
)
assertEquals(MainContentAllType.AUDIO, tab.type)
assertEquals("audio", port.lastListKind)
@@ -50,7 +57,15 @@ class MainContentAllQueryServiceTest {
val port = FakeMainContentAllQueryPort()
val service = createService(port)
val tab = service.getContents(type = "FREE", sort = "LATEST", dayOfWeek = null, page = 0, size = 20, member = null)
val tab = service.getContents(
type = "FREE",
sort = "LATEST",
dayOfWeek = null,
page = 0,
size = 20,
member = null,
lang = Lang.EN
)
assertEquals(MainContentAllType.FREE, tab.type)
assertEquals("audio", port.lastListKind)
@@ -69,7 +84,15 @@ class MainContentAllQueryServiceTest {
val port = FakeMainContentAllQueryPort()
val service = createService(port)
val tab = service.getContents(type = "POINT", sort = "PRICE_LOW", dayOfWeek = null, page = 0, size = 20, member = null)
val tab = service.getContents(
type = "POINT",
sort = "PRICE_LOW",
dayOfWeek = null,
page = 0,
size = 20,
member = null,
lang = Lang.EN
)
assertEquals(MainContentAllType.POINT, tab.type)
assertEquals("audio", port.lastListKind)
@@ -87,9 +110,9 @@ class MainContentAllQueryServiceTest {
@DisplayName("SERIES는 요일을 전달하고 ORIGINAL은 original 필터와 dayOfWeek null을 전달한다")
fun shouldQuerySeriesByType() {
val seriesPort = FakeMainContentAllQueryPort()
val service = createService(seriesPort, lang = Lang.JA)
val service = createService(seriesPort)
val seriesTab = service.getContents("SERIES", "POPULAR", "MON", 0, 20, null)
val seriesTab = service.getContents("SERIES", "POPULAR", "MON", 0, 20, null, Lang.JA)
assertEquals(MainContentAllType.SERIES, seriesTab.type)
assertEquals("series", seriesPort.lastListKind)
@@ -100,7 +123,7 @@ class MainContentAllQueryServiceTest {
val originalPort = FakeMainContentAllQueryPort()
val originalService = createService(originalPort)
val originalTab = originalService.getContents("ORIGINAL", "POPULAR", "MON", 0, 20, null)
val originalTab = originalService.getContents("ORIGINAL", "POPULAR", "MON", 0, 20, null, Lang.EN)
assertEquals(MainContentAllType.ORIGINAL, originalTab.type)
assertEquals("series", originalPort.lastListKind)
@@ -112,7 +135,7 @@ class MainContentAllQueryServiceTest {
@DisplayName("비회원은 성인 콘텐츠 비노출로 조회하고 회원은 preference 결과를 전달한다")
fun shouldPassAdultVisibilityByMember() {
val anonymousPort = FakeMainContentAllQueryPort()
createService(anonymousPort).getContents("AUDIO", null, null, null, null, null)
createService(anonymousPort).getContents("AUDIO", null, null, null, null, null, Lang.EN)
assertEquals(null, anonymousPort.lastMemberId)
assertFalse(anonymousPort.lastCanViewAdultContent)
@@ -127,7 +150,7 @@ class MainContentAllQueryServiceTest {
val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
Mockito.doReturn(true).`when`(preferenceService).canViewAdultContent(member)
createService(memberPort, preferenceService).getContents("AUDIO", null, null, null, null, member)
createService(memberPort, preferenceService).getContents("AUDIO", null, null, null, null, member, Lang.EN)
assertEquals(10L, memberPort.lastMemberId)
assertTrue(memberPort.lastCanViewAdultContent)
@@ -136,16 +159,12 @@ class MainContentAllQueryServiceTest {
private fun createService(
port: MainContentAllQueryPort,
preferenceService: MemberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java),
lang: Lang = Lang.EN
preferenceService: MemberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
): MainContentAllQueryService {
val langContext = LangContext()
langContext.setLang(lang)
return MainContentAllQueryService(
queryPort = port,
memberContentPreferenceService = preferenceService,
queryPolicy = MainContentAllQueryPolicy(),
langContext = langContext
queryPolicy = MainContentAllQueryPolicy()
)
}
}
@@ -204,6 +223,7 @@ private class FakeMainContentAllQueryPort : MainContentAllQueryPort {
lastOnlyPointAvailable = onlyPointAvailable
return (1L..limit.toLong()).map { id ->
MainContentAllAudio(
creatorMemberId = 10L,
audioContentId = id,
title = "audio-$id",
imageUrl = null,
@@ -253,6 +273,7 @@ private class FakeMainContentAllQueryPort : MainContentAllQueryPort {
lastLocale = locale
return listOf(
MainContentAllSeries(
creatorMemberId = 10L,
seriesId = 1L,
title = "series",
coverImageUrl = null,
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.content.ranking.adapter.out.persistence
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.content.translation.ContentTranslation
import kr.co.vividnext.sodalive.content.translation.ContentTranslationPayload
@@ -31,7 +32,11 @@ class DefaultAudioRankingSnapshotPersistenceAdapterTest @Autowired constructor(
private val entityManager: EntityManager,
private val entityManagerFactory: EntityManagerFactory
) {
private val adapter = DefaultAudioRankingSnapshotPersistenceAdapter(repository, contentTranslationRepository)
private val adapter = DefaultAudioRankingSnapshotPersistenceAdapter(
repository,
contentTranslationRepository,
JPAQueryFactory(entityManager)
)
@Test
@DisplayName("SQL NULL 번역 payload는 snapshot 제목으로 fallback한다")
@@ -1,7 +1,6 @@
package kr.co.vividnext.sodalive.v2.content.ranking.application
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.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingType
@@ -35,14 +34,11 @@ class AudioRankingQueryServiceTest {
"en" to originalLatestSnapshots.map { it.copy(title = "en-title-${it.contentId}") },
"ja" to originalLatestSnapshots.map { it.copy(title = "ja-title-${it.contentId}") }
)
val langContext = LangContext().apply { setLang(Lang.EN) }
val service = service(snapshotPort, langContext = langContext)
val service = service(snapshotPort)
val english = service.getRankings(AudioRankingType.REVENUE, member = null)
langContext.setLang(Lang.JA)
val japanese = service.getRankings(AudioRankingType.REVENUE, member = null)
langContext.setLang(Lang.EN)
val englishAgain = service.getRankings(AudioRankingType.REVENUE, member = null)
val english = service.getRankings(AudioRankingType.REVENUE, member = null, lang = Lang.EN)
val japanese = service.getRankings(AudioRankingType.REVENUE, member = null, lang = Lang.JA)
val englishAgain = service.getRankings(AudioRankingType.REVENUE, member = null, lang = Lang.EN)
assertEquals(listOf("en-title-2", "en-title-1"), english.items.map { it.title })
assertEquals(listOf("ja-title-2", "ja-title-1"), japanese.items.map { it.title })
@@ -64,7 +60,8 @@ class AudioRankingQueryServiceTest {
val method = AudioRankingQueryService::class.java.getDeclaredMethod(
"getRankings",
AudioRankingType::class.java,
Member::class.java
Member::class.java,
Lang::class.java
)
assertEquals(null, method.getAnnotation(Transactional::class.java))
@@ -84,7 +81,7 @@ class AudioRankingQueryServiceTest {
)
val service = service(snapshotPort)
val result = service.getRankings(AudioRankingType.REVENUE, member = null)
val result = service.getRankings(AudioRankingType.REVENUE, member = null, lang = Lang.KO)
assertTrue(result.showRankChange)
assertEquals(AudioRankingType.REVENUE, result.type)
@@ -106,7 +103,7 @@ class AudioRankingQueryServiceTest {
snapshotPort.latestSnapshots = listOf(snapshot(contentId = 1L, rank = 1))
val service = service(snapshotPort)
val result = service.getRankings(AudioRankingType.WEEKLY_POPULAR, member = null)
val result = service.getRankings(AudioRankingType.WEEKLY_POPULAR, member = null, lang = Lang.KO)
assertFalse(result.showRankChange)
assertEquals(listOf(1L), result.items.map { it.contentId })
@@ -116,7 +113,11 @@ class AudioRankingQueryServiceTest {
@Test
fun shouldReturnEmptyRankingWhenLatestSnapshotDoesNotExist() {
val result = service(FakeAudioRankingQuerySnapshotPort()).getRankings(AudioRankingType.LIKE_COUNT, member = null)
val result = service(FakeAudioRankingQuerySnapshotPort()).getRankings(
AudioRankingType.LIKE_COUNT,
member = null,
lang = Lang.KO
)
assertFalse(result.showRankChange)
assertEquals(AudioRankingType.LIKE_COUNT, result.type)
@@ -136,7 +137,7 @@ class AudioRankingQueryServiceTest {
snapshot(contentId = 2L, rank = 2)
)
val result = service(snapshotPort).getRankings(AudioRankingType.REVENUE, member = null)
val result = service(snapshotPort).getRankings(AudioRankingType.REVENUE, member = null, lang = Lang.KO)
assertEquals(listOf(2L, 3L), result.items.map { it.contentId })
assertEquals(listOf(1, 2), result.items.map { it.rank })
@@ -150,7 +151,7 @@ class AudioRankingQueryServiceTest {
val member = Mockito.mock(Member::class.java)
snapshotPort.latestSnapshots = listOf(snapshot(contentId = 1L, rank = 1, isAdult = true))
val result = service(snapshotPort, adultMember = member).getRankings(AudioRankingType.REVENUE, member)
val result = service(snapshotPort, adultMember = member).getRankings(AudioRankingType.REVENUE, member, Lang.KO)
assertEquals(listOf(1L), result.items.map { it.contentId })
}
@@ -170,7 +171,7 @@ class AudioRankingQueryServiceTest {
snapshot(contentId = 1L, rank = 2, creatorMemberId = 101L)
)
val result = service(snapshotPort, blockPort = blockPort).getRankings(AudioRankingType.REVENUE, member)
val result = service(snapshotPort, blockPort = blockPort).getRankings(AudioRankingType.REVENUE, member, Lang.KO)
assertEquals(listOf(1L, 3L), result.items.map { it.contentId })
assertEquals(listOf(1, 2), result.items.map { it.rank })
@@ -185,7 +186,11 @@ class AudioRankingQueryServiceTest {
val blockPort = FakeAudioRankingBlockPort(blockedCreatorMemberIds = setOf(101L))
snapshotPort.latestSnapshots = listOf(snapshot(contentId = 1L, rank = 1, creatorMemberId = 101L))
val result = service(snapshotPort, blockPort = blockPort).getRankings(AudioRankingType.REVENUE, member = null)
val result = service(snapshotPort, blockPort = blockPort).getRankings(
AudioRankingType.REVENUE,
member = null,
lang = Lang.KO
)
assertEquals(listOf(1L), result.items.map { it.contentId })
assertEquals(0, blockPort.callCount)
@@ -200,7 +205,11 @@ class AudioRankingQueryServiceTest {
listOf(snapshot(contentId = 1L, rank = 1))
)
val result = service(snapshotPort, jobService = jobService).getRankings(AudioRankingType.LIKE_COUNT, member = null)
val result = service(snapshotPort, jobService = jobService).getRankings(
AudioRankingType.LIKE_COUNT,
member = null,
lang = Lang.KO
)
assertEquals(listOf(1L), result.items.map { it.contentId })
assertEquals(2, snapshotPort.latestCallCount)
@@ -214,7 +223,11 @@ class AudioRankingQueryServiceTest {
Mockito.doThrow(IllegalStateException("aggregate failed"))
.`when`(jobService).refreshLastCompletedWeekByFallback(AudioRankingType.LIKE_COUNT)
val result = service(snapshotPort, jobService = jobService).getRankings(AudioRankingType.LIKE_COUNT, member = null)
val result = service(snapshotPort, jobService = jobService).getRankings(
AudioRankingType.LIKE_COUNT,
member = null,
lang = Lang.KO
)
assertFalse(result.showRankChange)
assertEquals(AudioRankingType.LIKE_COUNT, result.type)
@@ -230,7 +243,7 @@ class AudioRankingQueryServiceTest {
val jobService = Mockito.mock(AudioRankingSnapshotJobService::class.java)
snapshotPort.latestSnapshots = listOf(snapshot(contentId = 1L, rank = 1))
service(snapshotPort, jobService = jobService).getRankings(AudioRankingType.REVENUE, member = null)
service(snapshotPort, jobService = jobService).getRankings(AudioRankingType.REVENUE, member = null, lang = Lang.KO)
Mockito.verify(jobService, Mockito.never()).refreshLastCompletedWeekByFallback(AudioRankingType.REVENUE)
}
@@ -250,7 +263,7 @@ class AudioRankingQueryServiceTest {
snapshot(contentId = 2L, rank = 3, creatorMemberId = 102L)
)
val result = service(snapshotPort, blockPort = blockPort).getRankings(AudioRankingType.REVENUE, member)
val result = service(snapshotPort, blockPort = blockPort).getRankings(AudioRankingType.REVENUE, member, Lang.KO)
assertEquals(setOf(101L, 102L, 999L), blockPort.creatorMemberIds)
assertEquals(listOf(0, 0), result.items.map { it.rankChange })
@@ -261,8 +274,7 @@ class AudioRankingQueryServiceTest {
snapshotPort: FakeAudioRankingQuerySnapshotPort,
adultMember: Member? = null,
blockPort: AudioRankingBlockPort = FakeAudioRankingBlockPort(),
jobService: AudioRankingSnapshotJobService = Mockito.mock(AudioRankingSnapshotJobService::class.java),
langContext: LangContext = LangContext()
jobService: AudioRankingSnapshotJobService = Mockito.mock(AudioRankingSnapshotJobService::class.java)
): AudioRankingQueryService {
val memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
if (adultMember != null) {
@@ -270,7 +282,6 @@ class AudioRankingQueryServiceTest {
}
return AudioRankingQueryService(
snapshotPort = snapshotPort,
langContext = langContext,
memberContentPreferenceService = memberContentPreferenceService,
blockPort = blockPort,
jobService = jobService,
@@ -1,7 +1,6 @@
package kr.co.vividnext.sodalive.v2.content.recommendation.application
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.MemberRole
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
@@ -20,12 +19,10 @@ class AudioRecommendationQueryServiceTest {
private val queryPort = Mockito.mock(AudioRecommendationQueryPort::class.java)
private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
private val fallbackService = Mockito.mock(AudioRecommendationSnapshotFallbackService::class.java)
private val langContext = LangContext().apply { setLang(Lang.EN) }
private val service = AudioRecommendationQueryService(
queryPort,
preferenceService,
fallbackService,
langContext
fallbackService
)
@Test
@@ -63,7 +60,7 @@ class AudioRecommendationQueryServiceTest {
emptyList()
)
val recommendations = service.getRecommendations(null)
val recommendations = service.getRecommendations(null, Lang.EN)
assertEquals(0, recommendations.mostCommentedAudios.size)
verifyFallback(
@@ -134,7 +131,7 @@ class AudioRecommendationQueryServiceTest {
listOf(snapshot(RecommendedSectionType.RECOMMENDED_AUDIO_SAFE, 2L))
)
service.getRecommendations(null)
service.getRecommendations(null, Lang.EN)
verifyFallback(
RecommendedSectionType.MOST_COMMENTED_AUDIO_SAFE,
@@ -156,7 +153,7 @@ class AudioRecommendationQueryServiceTest {
listOf(snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 10L))
)
service.getRecommendations(member)
service.getRecommendations(member, Lang.EN)
Mockito.verify(preferenceService).canViewAdultContent(member)
Mockito.verify(queryPort).findBanners(AudioRecommendationQueryService.BANNER_LIMIT, member.id, true, Lang.JA)
@@ -180,7 +177,7 @@ class AudioRecommendationQueryServiceTest {
listOf(snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 10L))
)
service.getRecommendations(member)
service.getRecommendations(member, Lang.EN)
verifyFallback(
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
@@ -209,7 +206,7 @@ class AudioRecommendationQueryServiceTest {
eqValue(Lang.EN.code)
)
val result = service.findNewAndHotAudios(member, offset = 20L, limit = 21)
val result = service.findNewAndHotAudios(member, offset = 20L, limit = 21, lang = Lang.EN)
assertEquals(listOf(3L, 4L, 5L), result.map { it.audioContentId })
verifyFallback(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 20L, 21)
@@ -261,6 +258,7 @@ class AudioRecommendationQueryServiceTest {
private fun audioCard(id: Long): AudioCard {
return AudioCard(
creatorMemberId = id + 100L,
audioContentId = id,
title = "audio$id",
duration = "00:01",
@@ -14,6 +14,7 @@ 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.i18n.Lang
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.block.BlockMember
@@ -62,7 +63,7 @@ class DefaultCreatorChannelAudioQueryRepositoryTest @Autowired constructor(
saveBlock(creator, viewer)
flushAndClear()
val record = repository.findCreator(creator.id!!, viewer.id!!)
val record = repository.findCreator(creator.id!!, viewer.id!!, Lang.KO)
val themes = repository.findAudioThemes(creator.id!!, now, canViewAdultContent = false, "en")
assertEquals(creator.id, record!!.creatorId)
@@ -45,7 +45,8 @@ class CreatorChannelAudioQueryServiceTest {
themeId = 999L,
page = -1,
size = 100,
now = now
now = now,
lang = Lang.EN
)
assertEquals(ContentSort.LATEST, tab.sort)
@@ -78,7 +79,7 @@ class CreatorChannelAudioQueryServiceTest {
val service = createService(port)
val viewer = createMember(id = 10L)
val tab = service.getAudioTab(1L, viewer, null, null, null, null, LocalDateTime.of(2026, 6, 19, 10, 0))
val tab = service.getAudioTab(1L, viewer, null, null, null, null, LocalDateTime.of(2026, 6, 19, 10, 0), Lang.EN)
assertEquals(0.0, tab.purchasedAudioContentRate)
}
@@ -91,7 +92,7 @@ class CreatorChannelAudioQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getAudioTab(1L, viewer, null, null, null, null, LocalDateTime.of(2026, 6, 19, 10, 0))
service.getAudioTab(1L, viewer, null, null, null, null, LocalDateTime.of(2026, 6, 19, 10, 0), Lang.EN)
}
assertEquals("member.validation.user_not_found", exception.messageKey)
@@ -105,7 +106,7 @@ class CreatorChannelAudioQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getAudioTab(1L, viewer, null, null, null, null, LocalDateTime.of(2026, 6, 19, 10, 0))
service.getAudioTab(1L, viewer, null, null, null, null, LocalDateTime.of(2026, 6, 19, 10, 0), Lang.EN)
}
assertEquals("member.validation.creator_not_found", exception.messageKey)
@@ -119,7 +120,7 @@ class CreatorChannelAudioQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getAudioTab(1L, viewer, null, null, null, null, LocalDateTime.of(2026, 6, 19, 10, 0))
service.getAudioTab(1L, viewer, null, null, null, null, LocalDateTime.of(2026, 6, 19, 10, 0), Lang.EN)
}
assertNull(exception.messageKey)
@@ -191,7 +192,7 @@ private class FakeCreatorChannelAudioQueryPort : CreatorChannelAudioQueryPort {
var themeCanViewAdultContent: Boolean? = null
var themeLocale: String? = null
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelAudioCreatorRecord? = creator
override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelAudioCreatorRecord? = creator
override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean = blocked
@@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.CreatorCommunityComment
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.CreatorCommunityLike
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberKind
import kr.co.vividnext.sodalive.member.MemberRole
@@ -46,8 +47,8 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor(
val inactiveCreator = saveMember("inactive-creator", MemberRole.CREATOR, isActive = false)
flushAndClear()
val activeRecord = repository.findCreator(activeCreator.id!!, viewer.id!!)
val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!)
val activeRecord = repository.findCreator(activeCreator.id!!, viewer.id!!, Lang.KO)
val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!, Lang.KO)
assertNotNull(activeRecord)
assertEquals(activeCreator.id, activeRecord!!.creatorId)
@@ -92,7 +93,8 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor(
viewerId = viewer.id!!,
canViewAdultContent = false,
offset = 0,
limit = 10
limit = 10,
lang = Lang.KO
)
val visiblePostIds = visiblePosts.map { it.postId }
@@ -125,14 +127,16 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor(
viewerId = viewer.id!!,
canViewAdultContent = true,
offset = 0,
limit = 3
limit = 3,
lang = Lang.KO
)
val secondPage: List<CreatorChannelCommunityPostRecord> = repository.findCommunityPosts(
creatorId = creator.id!!,
viewerId = viewer.id!!,
canViewAdultContent = true,
offset = 3,
limit = 2
limit = 2,
lang = Lang.KO
)
assertEquals(
@@ -168,7 +172,8 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor(
viewerId = viewer.id!!,
canViewAdultContent = true,
offset = 0,
limit = 10
limit = 10,
lang = Lang.KO
)
val record = records.single { it.postId == post.id }
val otherMemberOnlyLikedRecord = records.single { it.postId == otherMemberOnlyLikedPost.id }
@@ -203,21 +208,24 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor(
viewerId = viewer.id!!,
canViewAdultContent = true,
offset = 0,
limit = 10
limit = 10,
lang = Lang.KO
).single()
val writerRecord = repository.findCommunityPosts(
creatorId = creator.id!!,
viewerId = secretWriter.id!!,
canViewAdultContent = true,
offset = 0,
limit = 10
limit = 10,
lang = Lang.KO
).single()
val creatorRecord = repository.findCommunityPosts(
creatorId = creator.id!!,
viewerId = creator.id!!,
canViewAdultContent = true,
offset = 0,
limit = 10
limit = 10,
lang = Lang.KO
).single()
assertEquals(1, viewerRecord.commentCount)
@@ -247,7 +255,8 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor(
viewerId = viewer.id!!,
canViewAdultContent = true,
offset = 0,
limit = 10
limit = 10,
lang = Lang.KO
)
val recordsById = records.associateBy { it.postId }
@@ -271,7 +280,8 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor(
viewerId = creator.id!!,
canViewAdultContent = true,
offset = 0,
limit = 10
limit = 10,
lang = Lang.KO
).single()
assertTrue(record.existOrdered)
@@ -297,14 +307,16 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor(
viewerId = viewer.id!!,
isPinned = true,
canViewAdultContent = false,
limit = 3
limit = 3,
lang = Lang.KO
)
val normalPosts: List<CreatorChannelCommunityPostRecord> = repository.findHomeCommunityPosts(
creatorId = creator.id!!,
viewerId = viewer.id!!,
isPinned = false,
canViewAdultContent = false,
limit = 3
limit = 3,
lang = Lang.KO
)
assertEquals(listOf(pinned.id), pinnedPosts.map { it.postId })
@@ -337,14 +349,16 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor(
viewerId = viewer.id!!,
isPinned = false,
canViewAdultContent = true,
limit = 10
limit = 10,
lang = Lang.KO
)
val adultBlockedPosts: List<CreatorChannelCommunityPostRecord> = repository.findHomeCommunityPosts(
creatorId = creator.id!!,
viewerId = viewer.id!!,
isPinned = false,
canViewAdultContent = false,
limit = 10
limit = 10,
lang = Lang.KO
)
assertTrue(inactivePurchasedPost.id in adultAllowedPosts.map { it.postId })
@@ -368,9 +382,19 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor(
saveCommunityComment(viewer, paidPost, isActive = true)
flushAndClear()
val record = repository.findCommunityPost(paidPost.id!!, viewer.id!!, canViewAdultContent = true)
val blockedAdultRecord = repository.findCommunityPost(adultPost.id!!, viewer.id!!, canViewAdultContent = false)
val inactiveRecord = repository.findCommunityPost(inactivePost.id!!, viewer.id!!, canViewAdultContent = true)
val record = repository.findCommunityPost(paidPost.id!!, viewer.id!!, canViewAdultContent = true, lang = Lang.KO)
val blockedAdultRecord = repository.findCommunityPost(
adultPost.id!!,
viewer.id!!,
canViewAdultContent = false,
lang = Lang.KO
)
val inactiveRecord = repository.findCommunityPost(
inactivePost.id!!,
viewer.id!!,
canViewAdultContent = true,
lang = Lang.KO
)
assertNotNull(record)
assertEquals(paidPost.id, record!!.postId)
@@ -412,8 +436,15 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor(
val viewerCount = repository.countCommunityComments(post.id!!, viewer.id!!, creator.id!!)
val creatorCount = repository.countCommunityComments(post.id!!, creator.id!!, creator.id!!)
val comments = repository.findCommunityComments(post.id!!, viewer.id!!, creator.id!!, offset = 0, limit = 10)
val latestReplies = repository.findLatestRepliesByCommentIds(comments.map { it.commentId }, viewer.id!!)
val comments = repository.findCommunityComments(
post.id!!,
viewer.id!!,
creator.id!!,
offset = 0,
limit = 10,
lang = Lang.KO
)
val latestReplies = repository.findLatestRepliesByCommentIds(comments.map { it.commentId }, viewer.id!!, Lang.KO)
assertEquals(2, viewerCount)
assertEquals(4, creatorCount)
@@ -448,7 +479,7 @@ class DefaultCreatorChannelCommunityQueryRepositoryTest @Autowired constructor(
val invisibleSecretContext = repository.findRootCommentContext(secretRoot.id!!, viewer.id!!, canViewAdultContent = true)
val replyContext = repository.findRootCommentContext(newReply.id!!, viewer.id!!, canViewAdultContent = true)
val count = repository.countCommunityReplies(root.id!!, viewer.id!!)
val replies = repository.findCommunityReplies(root.id!!, viewer.id!!, offset = 0, limit = 10)
val replies = repository.findCommunityReplies(root.id!!, viewer.id!!, offset = 0, limit = 10, lang = Lang.KO)
assertNotNull(context)
assertEquals(post.id, context!!.postId)
@@ -47,7 +47,8 @@ class CreatorChannelCommunityQueryServiceTest {
viewer = viewer,
page = -1,
size = 100,
now = now
now = now,
lang = Lang.EN
)
assertEquals(60, tab.communityPostCount)
@@ -72,7 +73,7 @@ class CreatorChannelCommunityQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getCommunityTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0))
service.getCommunityTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN)
}
assertEquals("member.validation.user_not_found", exception.messageKey)
@@ -86,7 +87,7 @@ class CreatorChannelCommunityQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getCommunityTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0))
service.getCommunityTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN)
}
assertEquals("member.validation.creator_not_found", exception.messageKey)
@@ -100,7 +101,7 @@ class CreatorChannelCommunityQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getCommunityTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0))
service.getCommunityTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN)
}
assertNull(exception.messageKey)
@@ -129,7 +130,7 @@ class CreatorChannelCommunityQueryServiceTest {
val service = createService(port, audioContentCloudFront)
val viewer = createMember(id = 10L)
val posts = service.getCommunityTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0))
val posts = service.getCommunityTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN)
.communityPosts
assertEquals("https://cdn.test/image/1.png", posts[0].imageUrl)
@@ -165,7 +166,8 @@ class CreatorChannelCommunityQueryServiceTest {
viewerId = 10L,
isPinned = true,
canViewAdultContent = false,
limit = 3
limit = 3,
lang = Lang.KO
)
assertEquals(1, posts.size)
@@ -203,7 +205,7 @@ class CreatorChannelCommunityQueryServiceTest {
)
)
val posts = service.getCommunityTab(1L, createMember(id = 10L), 0, 20).communityPosts
val posts = service.getCommunityTab(1L, createMember(id = 10L), 0, 20, lang = Lang.EN).communityPosts
assertEquals(
listOf(
@@ -230,7 +232,7 @@ class CreatorChannelCommunityQueryServiceTest {
val returnedPostIds = (1L..50L).toList()
Mockito.`when`(translationService.findDisplayContents(returnedPostIds, "en")).thenReturn(emptyMap())
val tab = service.getCommunityTab(1L, createMember(id = 10L), page = 0, size = 50)
val tab = service.getCommunityTab(1L, createMember(id = 10L), page = 0, size = 50, lang = Lang.EN)
assertEquals(50, tab.communityPosts.size)
assertTrue(tab.hasNext)
@@ -248,13 +250,13 @@ class CreatorChannelCommunityQueryServiceTest {
Mockito.`when`(translationService.findDisplayContents(listOf(21L), "en"))
.thenReturn(mapOf(21L to CreatorCommunityDisplayContent("translated pinned", true)))
val pinnedPosts = service.findHomeCommunityPosts(1L, 10L, true, false, 3)
val pinnedPosts = service.findHomeCommunityPosts(1L, 10L, true, false, 3, Lang.EN)
port.homeCommunityPosts = listOf(communityPostRecord(22L, price = 0).copy(content = "source normal"))
Mockito.`when`(translationService.findDisplayContents(listOf(22L), "en"))
.thenReturn(mapOf(22L to CreatorCommunityDisplayContent("translated normal", true)))
val normalPosts = service.findHomeCommunityPosts(1L, 10L, false, false, 3)
val normalPosts = service.findHomeCommunityPosts(1L, 10L, false, false, 3, Lang.EN)
assertEquals("translated pinned", pinnedPosts.single().content)
assertEquals("translated normal", normalPosts.single().content)
@@ -275,7 +277,7 @@ class CreatorChannelCommunityQueryServiceTest {
val service = createService(port)
val viewer = createMember(id = 10L)
val detail = service.getCommunityPostDetail(1L, viewer, LocalDateTime.of(2026, 7, 6, 10, 0))
val detail = service.getCommunityPostDetail(1L, viewer, LocalDateTime.of(2026, 7, 6, 10, 0), Lang.KO)
assertEquals(1L, detail.post.postId)
assertEquals(true, detail.post.isLiked)
@@ -298,7 +300,7 @@ class CreatorChannelCommunityQueryServiceTest {
Mockito.`when`(translationService.findDisplayContents(listOf(1L), "en"))
.thenReturn(mapOf(1L to CreatorCommunityDisplayContent("content-1", true)))
service.getCommunityPostDetail(1L, createMember(id = 10L), LocalDateTime.of(2026, 7, 6, 10, 0))
service.getCommunityPostDetail(1L, createMember(id = 10L), LocalDateTime.of(2026, 7, 6, 10, 0), Lang.KO)
Mockito.verify(translationService, Mockito.never()).requestTranslations(1L, "en")
}
@@ -311,7 +313,14 @@ class CreatorChannelCommunityQueryServiceTest {
}
val service = createService(port)
val comments = service.getCommunityComments(1L, createMember(id = 10L), -1, 100, LocalDateTime.of(2026, 7, 6, 10, 0))
val comments = service.getCommunityComments(
1L,
createMember(id = 10L),
-1,
100,
LocalDateTime.of(2026, 7, 6, 10, 0),
Lang.KO
)
assertEquals(0, comments.commentCount)
assertEquals(0, comments.comments.size)
@@ -332,7 +341,14 @@ class CreatorChannelCommunityQueryServiceTest {
}
val service = createService(port)
val comments = service.getCommunityComments(1L, createMember(id = 10L), -1, 100, LocalDateTime.of(2026, 7, 6, 10, 0))
val comments = service.getCommunityComments(
1L,
createMember(id = 10L),
-1,
100,
LocalDateTime.of(2026, 7, 6, 10, 0),
Lang.KO
)
assertEquals(0L, port.commentListOffset)
assertEquals(51, port.commentListLimit)
@@ -356,7 +372,14 @@ class CreatorChannelCommunityQueryServiceTest {
}
val service = createService(port)
val replies = service.getCommunityReplies(1L, createMember(id = 10L), -1, 100, LocalDateTime.of(2026, 7, 6, 10, 0))
val replies = service.getCommunityReplies(
1L,
createMember(id = 10L),
-1,
100,
LocalDateTime.of(2026, 7, 6, 10, 0),
Lang.KO
)
assertEquals(51, replies.replyCount)
assertEquals(50, replies.replies.size)
@@ -373,7 +396,7 @@ class CreatorChannelCommunityQueryServiceTest {
val service = createService(port)
val exception = assertThrows(SodaException::class.java) {
service.getCommunityReplies(1L, createMember(id = 10L), null, null, LocalDateTime.of(2026, 7, 6, 10, 0))
service.getCommunityReplies(1L, createMember(id = 10L), null, null, LocalDateTime.of(2026, 7, 6, 10, 0), Lang.KO)
}
assertEquals("creator.community.invalid_request_retry", exception.messageKey)
@@ -389,7 +412,14 @@ class CreatorChannelCommunityQueryServiceTest {
}
val service = createService(port)
val replies = service.getCommunityReplies(1L, createMember(id = 10L), null, null, LocalDateTime.of(2026, 7, 6, 10, 0))
val replies = service.getCommunityReplies(
1L,
createMember(id = 10L),
null,
null,
LocalDateTime.of(2026, 7, 6, 10, 0),
Lang.KO
)
assertEquals(0, replies.replyCount)
assertEquals(0, replies.replies.size)
@@ -484,7 +514,7 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer
var homeCanViewAdultContent: Boolean? = null
var homeLimit: Int? = null
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCommunityCreatorRecord? = creator
override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelCommunityCreatorRecord? = creator
override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean = blocked
@@ -502,7 +532,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer
viewerId: Long,
canViewAdultContent: Boolean,
offset: Long,
limit: Int
limit: Int,
lang: Lang
): List<CreatorChannelCommunityPostRecord> {
listCanViewAdultContent = canViewAdultContent
listOffset = offset
@@ -515,7 +546,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer
viewerId: Long,
isPinned: Boolean,
canViewAdultContent: Boolean,
limit: Int
limit: Int,
lang: Lang
): List<CreatorChannelCommunityPostRecord> {
homeCreatorId = creatorId
homeViewerId = viewerId
@@ -528,7 +560,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer
override fun findCommunityPost(
postId: Long,
viewerId: Long,
canViewAdultContent: Boolean
canViewAdultContent: Boolean,
lang: Lang
): CreatorChannelCommunityPostRecord? = communityPost
override fun countCommunityComments(postId: Long, viewerId: Long, creatorId: Long): Int = communityCommentCount
@@ -538,7 +571,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer
viewerId: Long,
creatorId: Long,
offset: Long,
limit: Int
limit: Int,
lang: Lang
): List<CreatorChannelCommunityCommentRecord> {
commentListCalled = true
commentListOffset = offset
@@ -548,7 +582,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer
override fun findLatestRepliesByCommentIds(
commentIds: List<Long>,
viewerId: Long
viewerId: Long,
lang: Lang
): List<CreatorChannelCommunityCommentRecord> {
latestReplyCommentIds = commentIds
return latestReplies
@@ -566,7 +601,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer
commentId: Long,
viewerId: Long,
offset: Long,
limit: Int
limit: Int,
lang: Lang
): List<CreatorChannelCommunityCommentRecord> {
replyListOffset = offset
replyListLimit = limit
@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persist
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationMessage
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.DonationRankingPeriod
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
@@ -48,9 +49,9 @@ class DefaultCreatorChannelDonationQueryRepositoryTest @Autowired constructor(
val nonCreator = saveMember("donation-non-creator", MemberRole.USER)
flushAndClear()
val creatorRecord = repository.findCreator(creator.id!!, viewer.id!!)
val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!)
val nonCreatorRecord = repository.findCreator(nonCreator.id!!, viewer.id!!)
val creatorRecord = repository.findCreator(creator.id!!, viewer.id!!, Lang.KO)
val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!, Lang.KO)
val nonCreatorRecord = repository.findCreator(nonCreator.id!!, viewer.id!!, Lang.KO)
assertNotNull(creatorRecord)
assertEquals(creator.id, creatorRecord!!.creatorId)
@@ -91,7 +92,7 @@ class DefaultCreatorChannelDonationQueryRepositoryTest @Autowired constructor(
flushAndClear()
val count = repository.countChannelDonations(creator.id!!, creator.id!!, now)
val records = repository.findChannelDonations(creator.id!!, creator.id!!, now, offset = 0, limit = 10)
val records = repository.findChannelDonations(creator.id!!, creator.id!!, now, offset = 0, limit = 10, lang = Lang.KO)
assertEquals(2, count)
assertEquals(listOf(secret.can, monthStart.can), records.map { it.can })
@@ -121,7 +122,7 @@ class DefaultCreatorChannelDonationQueryRepositoryTest @Autowired constructor(
flushAndClear()
val count = repository.countChannelDonations(creator.id!!, viewer.id!!, now)
val records = repository.findChannelDonations(creator.id!!, viewer.id!!, now, offset = 0, limit = 10)
val records = repository.findChannelDonations(creator.id!!, viewer.id!!, now, offset = 0, limit = 10, lang = Lang.KO)
assertEquals(2, count)
assertEquals(listOf(ownSecretDonation.can, publicDonation.can), records.map { it.can })
@@ -141,7 +142,7 @@ class DefaultCreatorChannelDonationQueryRepositoryTest @Autowired constructor(
saveDonation(creator, donor, 300, sameCreatedAt.plusMinutes(1), additionalMessage = "newest")
flushAndClear()
val records = repository.findChannelDonations(creator.id!!, viewer.id!!, now, offset = 1, limit = 2)
val records = repository.findChannelDonations(creator.id!!, viewer.id!!, now, offset = 1, limit = 2, lang = Lang.KO)
assertEquals(listOf(second.can, first.can), records.map { it.can })
}
@@ -1,7 +1,7 @@
package kr.co.vividnext.sodalive.v2.creator.channel.donation.application
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.DonationRankingPeriod
import kr.co.vividnext.sodalive.member.Member
@@ -34,7 +34,8 @@ class CreatorChannelDonationQueryServiceTest {
viewer = createMember(VIEWER_ID),
page = 0,
size = 20,
now = NOW
now = NOW,
lang = Lang.KO
)
}
@@ -55,7 +56,8 @@ class CreatorChannelDonationQueryServiceTest {
viewer = createMember(VIEWER_ID),
page = 0,
size = 20,
now = NOW
now = NOW,
lang = Lang.KO
)
}
@@ -74,7 +76,8 @@ class CreatorChannelDonationQueryServiceTest {
viewer = createMember(VIEWER_ID),
page = 0,
size = 20,
now = NOW
now = NOW,
lang = Lang.KO
)
}
@@ -97,7 +100,8 @@ class CreatorChannelDonationQueryServiceTest {
viewer = createMember(VIEWER_ID),
page = -1,
size = 10,
now = NOW
now = NOW,
lang = Lang.KO
)
assertEquals(0L, queryPort.lastFindDonationRequest?.offset)
@@ -138,7 +142,8 @@ class CreatorChannelDonationQueryServiceTest {
viewer = createMember(VIEWER_ID),
page = 0,
size = 20,
now = NOW
now = NOW,
lang = Lang.KO
)
assertEquals("donor", tab.donations[0].nickname)
@@ -163,7 +168,8 @@ class CreatorChannelDonationQueryServiceTest {
viewer = createMember(CREATOR_ID),
page = 0,
size = 20,
now = NOW
now = NOW,
lang = Lang.KO
)
assertEquals(RankingRequest(CREATOR_ID, DonationRankingPeriod.WEEKLY, true), rankingPort.requests.single())
@@ -186,7 +192,7 @@ class CreatorChannelDonationQueryServiceTest {
)
),
rankingPort = weeklyRankingPort
).getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW)
).getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW, Lang.KO)
val cumulativeRankingPort = FakeDonationRankingPort()
createService(
@@ -197,7 +203,7 @@ class CreatorChannelDonationQueryServiceTest {
)
),
rankingPort = cumulativeRankingPort
).getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW)
).getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW, Lang.KO)
assertEquals(RankingRequest(CREATOR_ID, DonationRankingPeriod.WEEKLY, false), weeklyRankingPort.requests.single())
assertEquals(RankingRequest(CREATOR_ID, DonationRankingPeriod.CUMULATIVE, false), cumulativeRankingPort.requests.single())
@@ -214,7 +220,7 @@ class CreatorChannelDonationQueryServiceTest {
rankingPort = rankingPort
)
service.getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW)
service.getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW, Lang.KO)
assertEquals(RankingRequest(CREATOR_ID, DonationRankingPeriod.CUMULATIVE, false), rankingPort.requests.single())
}
@@ -233,7 +239,7 @@ class CreatorChannelDonationQueryServiceTest {
val rankingPort = FakeDonationRankingPort()
val service = createService(queryPort = queryPort, rankingPort = rankingPort)
val tab = service.getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW)
val tab = service.getDonationTab(CREATOR_ID, createMember(VIEWER_ID), 0, 20, NOW, Lang.KO)
assertEquals(emptyList<RankingRequest>(), rankingPort.requests)
assertEquals(emptyList<Any>(), tab.rankings)
@@ -255,7 +261,6 @@ class CreatorChannelDonationQueryServiceTest {
rankingPort = rankingPort,
queryPolicy = CreatorChannelDonationQueryPolicy(),
messageSource = SodaMessageSource(),
langContext = LangContext(),
cloudFrontHost = "https://cdn.test"
)
}
@@ -282,6 +287,7 @@ class CreatorChannelDonationQueryServiceTest {
createdAt: LocalDateTime = NOW
): CreatorChannelDonationRecord {
return CreatorChannelDonationRecord(
memberId = VIEWER_ID,
nickname = nickname,
profileImagePath = profileImagePath,
can = can,
@@ -319,7 +325,7 @@ class CreatorChannelDonationQueryServiceTest {
var lastFindDonationRequest: FindDonationRequest? = null
private set
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelDonationCreatorRecord? {
override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelDonationCreatorRecord? {
return creator
}
@@ -337,7 +343,8 @@ class CreatorChannelDonationQueryServiceTest {
viewerId: Long,
now: LocalDateTime,
offset: Long,
limit: Int
limit: Int,
lang: Lang
): List<CreatorChannelDonationRecord> {
lastFindDonationRequest = FindDonationRequest(creatorId, viewerId, now, offset, limit)
return donations
@@ -78,6 +78,7 @@ class CreatorChannelDonationQueryPolicyTest {
donationCan = 100
)
val donation = CreatorChannelDonation(
memberId = 10L,
nickname = "donor",
profileImageUrl = "https://cdn.test/donor.png",
can = 50,
@@ -99,6 +100,7 @@ class CreatorChannelDonationQueryPolicyTest {
donationRankingPeriod = DonationRankingPeriod.CUMULATIVE
)
val donationRecord = CreatorChannelDonationRecord(
memberId = 10L,
nickname = "donor",
profileImagePath = null,
can = 50,
@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.adapter.out.persiste
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers
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
@@ -41,9 +42,9 @@ class DefaultCreatorChannelFanTalkQueryRepositoryTest @Autowired constructor(
val nonCreator = saveMember("fantalk-non-creator", MemberRole.USER)
flushAndClear()
val activeRecord = repository.findCreator(activeCreator.id!!, viewer.id!!)
val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!)
val nonCreatorRecord = repository.findCreator(nonCreator.id!!, viewer.id!!)
val activeRecord = repository.findCreator(activeCreator.id!!, viewer.id!!, Lang.KO)
val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!, Lang.KO)
val nonCreatorRecord = repository.findCreator(nonCreator.id!!, viewer.id!!, Lang.KO)
assertNotNull(activeRecord)
assertEquals(activeCreator.id, activeRecord!!.creatorId)
@@ -90,8 +91,8 @@ class DefaultCreatorChannelFanTalkQueryRepositoryTest @Autowired constructor(
flushAndClear()
val count = repository.countFanTalks(creator.id!!, viewer.id!!)
val firstPage = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 0, limit = 1)
val secondPage = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 1, limit = 2)
val firstPage = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 0, limit = 1, lang = Lang.KO)
val secondPage = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 1, limit = 2, lang = Lang.KO)
assertEquals(2, count)
assertEquals(listOf(newer.id), firstPage.map { it.fanTalkId })
@@ -114,7 +115,7 @@ class DefaultCreatorChannelFanTalkQueryRepositoryTest @Autowired constructor(
val newest = saveCheers(writer, creator, "newest", isActive = true, createdAt = sameCreatedAt.plusMinutes(1))
flushAndClear()
val records = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 0, limit = 10)
val records = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 0, limit = 10, lang = Lang.KO)
assertEquals(listOf(newest.id, second.id, first.id), records.map { it.fanTalkId })
}
@@ -157,8 +158,8 @@ class DefaultCreatorChannelFanTalkQueryRepositoryTest @Autowired constructor(
)
flushAndClear()
val replies = repository.findCreatorReplies(creator.id!!, listOf(parent.id!!))
val emptyReplies = repository.findCreatorReplies(creator.id!!, emptyList())
val replies = repository.findCreatorReplies(creator.id!!, listOf(parent.id!!), Lang.KO)
val emptyReplies = repository.findCreatorReplies(creator.id!!, emptyList(), Lang.KO)
assertEquals(listOf(olderReply.id, newerReply.id), replies.map { it.fanTalkId })
assertEquals(parent.id, replies.first().parentFanTalkId)
@@ -2,7 +2,6 @@ package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.application
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
import kr.co.vividnext.sodalive.member.MemberProvider
@@ -30,7 +29,7 @@ class CreatorChannelFanTalkQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0))
service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN)
}
assertEquals("member.validation.user_not_found", exception.messageKey)
@@ -44,7 +43,7 @@ class CreatorChannelFanTalkQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0))
service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN)
}
assertEquals("member.validation.creator_not_found", exception.messageKey)
@@ -58,7 +57,7 @@ class CreatorChannelFanTalkQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0))
service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN)
}
assertNull(exception.messageKey)
@@ -85,7 +84,8 @@ class CreatorChannelFanTalkQueryServiceTest {
viewer = viewer,
page = -1,
size = 10,
now = LocalDateTime.of(2026, 6, 21, 10, 0)
now = LocalDateTime.of(2026, 6, 21, 10, 0),
lang = Lang.EN
)
assertEquals(60, tab.fanTalkCount)
@@ -114,7 +114,7 @@ class CreatorChannelFanTalkQueryServiceTest {
val service = createService(port)
val viewer = createMember(id = 10L)
val tab = service.getFanTalkTab(1L, viewer, 3, 20, LocalDateTime.of(2026, 6, 21, 10, 0))
val tab = service.getFanTalkTab(1L, viewer, 3, 20, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN)
assertEquals(5, tab.fanTalkCount)
assertEquals(emptyList<Long>(), tab.fanTalks.map { it.fanTalkId })
@@ -156,7 +156,7 @@ class CreatorChannelFanTalkQueryServiceTest {
val service = createService(port)
val viewer = createMember(id = 10L)
val fanTalks = service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0))
val fanTalks = service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0), Lang.EN)
.fanTalks
assertEquals("fan", fanTalks[0].writerNickname)
@@ -169,13 +169,10 @@ class CreatorChannelFanTalkQueryServiceTest {
}
private fun createService(port: FakeCreatorChannelFanTalkQueryPort): CreatorChannelFanTalkQueryService {
val langContext = LangContext()
langContext.setLang(Lang.EN)
return CreatorChannelFanTalkQueryService(
queryPortProvider = FixedCreatorChannelFanTalkQueryPortProvider(port),
queryPolicy = CreatorChannelFanTalkQueryPolicy(),
messageSource = SodaMessageSource(),
langContext = langContext,
cloudFrontHost = "https://cdn.test"
)
}
@@ -216,7 +213,7 @@ private class FakeCreatorChannelFanTalkQueryPort : CreatorChannelFanTalkQueryPor
var listLimit: Int? = null
var replyParentFanTalkIds: List<Long>? = null
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelFanTalkCreatorRecord? = creator
override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelFanTalkCreatorRecord? = creator
override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean = blocked
@@ -226,7 +223,8 @@ private class FakeCreatorChannelFanTalkQueryPort : CreatorChannelFanTalkQueryPor
creatorId: Long,
viewerId: Long,
offset: Long,
limit: Int
limit: Int,
lang: Lang
): List<CreatorChannelFanTalkRecord> {
listOffset = offset
listLimit = limit
@@ -235,7 +233,8 @@ private class FakeCreatorChannelFanTalkQueryPort : CreatorChannelFanTalkQueryPor
override fun findCreatorReplies(
creatorId: Long,
parentFanTalkIds: List<Long>
parentFanTalkIds: List<Long>,
lang: Lang
): List<CreatorChannelFanTalkReplyRecord> {
replyParentFanTalkIds = parentFanTalkIds
return creatorReplies
@@ -18,6 +18,7 @@ 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.explorer.profile.channelDonation.ChannelDonationMessage
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.live.room.GenderRestriction
import kr.co.vividnext.sodalive.live.room.LiveRoom
import kr.co.vividnext.sodalive.live.room.visit.LiveRoomVisit
@@ -70,8 +71,8 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
val aiCreator = saveMember("ai-creator", MemberRole.CREATOR, memberKind = MemberKind.AI_CHARACTER)
flushAndClear()
val record = repository.findCreator(creator.id!!, viewer.id!!)
val aiRecord = repository.findCreator(aiCreator.id!!, viewer.id!!)
val record = repository.findCreator(creator.id!!, viewer.id!!, Lang.KO)
val aiRecord = repository.findCreator(aiCreator.id!!, viewer.id!!, Lang.KO)
assertNotNull(record)
assertEquals(2, record!!.followerCount)
@@ -91,7 +92,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
saveCharacter(creator, isActive = false)
flushAndClear()
val record = repository.findCreator(creator.id!!, viewerId = null)
val record = repository.findCreator(creator.id!!, viewerId = null, lang = Lang.KO)
assertEquals(null, record!!.characterId)
assertFalse(record.isAiChatAvailable)
@@ -108,7 +109,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
saveFollowing(saveMember("inactive-member-follower", MemberRole.USER, isActive = false), creator, isActive = true)
flushAndClear()
val record = repository.findCreator(creator.id!!, viewerId = null)
val record = repository.findCreator(creator.id!!, viewerId = null, lang = Lang.KO)
assertEquals(5, record!!.followerCount)
}
@@ -119,7 +120,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
val user = saveMember("non-creator-target", MemberRole.USER)
flushAndClear()
val record = repository.findCreator(user.id!!, viewerId = null)
val record = repository.findCreator(user.id!!, viewerId = null, lang = Lang.KO)
assertNotNull(record)
assertEquals(MemberRole.USER, record!!.role)
@@ -216,7 +217,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
updateUpdatedAt("LiveRoom", currentLive.id!!, now.minusHours(1))
flushAndClear()
val creatorRecord = repository.findCreator(creator.id!!, viewer.id!!)
val creatorRecord = repository.findCreator(creator.id!!, viewer.id!!, Lang.KO)
val currentLiveRecord = repository.findCurrentLive(
creator.id!!,
now,
@@ -231,7 +232,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
canViewAdultContent = false,
viewerId = viewer.id!!
)
val donations = repository.findChannelDonations(creator.id!!, viewer.id!!, now, limit = 8)
val donations = repository.findChannelDonations(creator.id!!, viewer.id!!, now, limit = 8, lang = Lang.KO)
val schedules = repository.findSchedules(
creator.id!!,
now,
@@ -250,7 +251,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
limit = 9
)
val seriesRecords = repository.findSeries(creator.id!!, viewer.id!!, now, false, ContentType.ALL, limit = 8)
val fanTalkSummary = repository.findFanTalkSummary(creator.id!!, viewer.id!!)
val fanTalkSummary = repository.findFanTalkSummary(creator.id!!, viewer.id!!, Lang.KO)
val activity = repository.findActivity(creator.id!!, now)
val sns = repository.findSns(creator.id!!)
@@ -289,7 +290,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
saveFollowing(viewer, creator, isActive = false, isNotify = true)
flushAndClear()
val record = repository.findCreator(creator.id!!, viewer.id!!)
val record = repository.findCreator(creator.id!!, viewer.id!!, Lang.KO)
assertFalse(record!!.isFollow)
assertFalse(record.isNotify)
@@ -770,7 +771,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
saveCheers(writer, creator, "reply", isActive = true, LocalDateTime.of(2026, 6, 12, 14, 0), parent = older)
flushAndClear()
val summary = repository.findFanTalkSummary(creator.id!!, viewer.id!!)
val summary = repository.findFanTalkSummary(creator.id!!, viewer.id!!, Lang.KO)
assertEquals(2, summary.totalCount)
assertEquals(latest.id, summary.latestFanTalk!!.fanTalkId)
@@ -788,9 +789,9 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
val thirdParty = saveMember("kst-donation-third-party", MemberRole.USER)
flushAndClear()
val creatorView = repository.findChannelDonations(creator.id!!, creator.id!!, now, limit = 8)
val donorView = repository.findChannelDonations(creator.id!!, donor.id!!, now, limit = 8)
val thirdPartyView = repository.findChannelDonations(creator.id!!, thirdParty.id!!, now, limit = 8)
val creatorView = repository.findChannelDonations(creator.id!!, creator.id!!, now, limit = 8, lang = Lang.KO)
val donorView = repository.findChannelDonations(creator.id!!, donor.id!!, now, limit = 8, lang = Lang.KO)
val thirdPartyView = repository.findChannelDonations(creator.id!!, thirdParty.id!!, now, limit = 8, lang = Lang.KO)
assertEquals(listOf(300), creatorView.map { it.can })
assertEquals(listOf(300), donorView.map { it.can })
@@ -821,7 +822,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
)
flushAndClear()
val records = repository.findChannelDonations(creator.id!!, donor.id!!, now, limit = 8)
val records = repository.findChannelDonations(creator.id!!, donor.id!!, now, limit = 8, lang = Lang.KO)
assertEquals(listOf(1000, 3), records.map { it.can })
assertEquals("응원합니다", records.first().message)
@@ -837,7 +838,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
saveDonation(creator, donor, 1000, now.minusMinutes(1), isSecret = true, additionalMessage = "cheer")
flushAndClear()
val records = repository.findChannelDonations(creator.id!!, donor.id!!, now, limit = 8)
val records = repository.findChannelDonations(creator.id!!, donor.id!!, now, limit = 8, lang = Lang.KO)
assertEquals("cheer", records.single().message)
}
@@ -852,7 +853,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
saveBlock(writer, viewer)
flushAndClear()
val summary = repository.findFanTalkSummary(creator.id!!, viewer.id!!)
val summary = repository.findFanTalkSummary(creator.id!!, viewer.id!!, Lang.KO)
assertEquals(0, summary.totalCount)
assertEquals(null, summary.latestFanTalk)
@@ -1042,7 +1043,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
saveBlock(writerBlockedViewer, viewer)
flushAndClear()
val summary = repository.findFanTalkSummary(creator.id!!, viewer.id!!)
val summary = repository.findFanTalkSummary(creator.id!!, viewer.id!!, Lang.KO)
assertEquals(1, summary.totalCount)
assertEquals(visibleTalk.id, summary.latestFanTalk!!.fanTalkId)
@@ -1057,7 +1058,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
saveCheers(writer, creator, "visible", isActive = true, LocalDateTime.of(2026, 6, 12, 12, 0))
flushAndClear()
val summary = repository.findFanTalkSummary(creator.id!!, viewer.id!!)
val summary = repository.findFanTalkSummary(creator.id!!, viewer.id!!, Lang.KO)
assertEquals("fan", summary.latestFanTalk!!.nickname)
}
@@ -1085,7 +1086,7 @@ class DefaultCreatorChannelHomeQueryRepositoryTest @Autowired constructor(
updateUpdatedAt("LiveRoom", futureLive.id!!, now.plusDays(1).plusHours(1))
flushAndClear()
val creatorRecord = repository.findCreator(creator.id!!, viewerId = null)
val creatorRecord = repository.findCreator(creator.id!!, viewerId = null, lang = Lang.KO)
val activity = repository.findActivity(creator.id!!, now)
val sns = repository.findSns(creator.id!!)
@@ -70,7 +70,7 @@ class CreatorChannelHomeQueryServiceTest {
val viewer = createMember(id = 10L, gender = Gender.FEMALE, authGender = 1)
val now = LocalDateTime.of(2026, 6, 13, 10, 0)
val home = service.getHome(creatorId = 1L, viewer = viewer, now = now)
val home = service.getHome(creatorId = 1L, viewer = viewer, now = now, lang = Lang.KO)
assertEquals(1L, home.creator.creatorId)
assertEquals("https://cdn.test/profile/creator.png", home.creator.profileImageUrl)
@@ -115,7 +115,7 @@ class CreatorChannelHomeQueryServiceTest {
val service = createService(port, lang = Lang.JA)
val viewer = createMember(id = 10L)
service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0))
service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.JA)
assertEquals("ja", port.latestAudioLocale)
assertEquals("ja", port.audioContentsLocale)
@@ -130,7 +130,7 @@ class CreatorChannelHomeQueryServiceTest {
val service = createService(port)
val viewer = createMember(id = 1L, gender = Gender.FEMALE, authGender = null, role = MemberRole.CREATOR)
service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0))
service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO)
assertTrue(port.currentLiveIsViewerCreator == true)
assertTrue(port.schedulesIsViewerCreator == true)
@@ -145,7 +145,7 @@ class CreatorChannelHomeQueryServiceTest {
val service = createService(port)
val viewer = createMember(id = 10L, role = MemberRole.CREATOR)
service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0))
service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO)
assertTrue(port.currentLiveIsViewerCreator == true)
assertTrue(port.schedulesIsViewerCreator == true)
@@ -158,7 +158,7 @@ class CreatorChannelHomeQueryServiceTest {
val service = createService(port)
val viewer = createMember(id = 10L, role = MemberRole.USER)
service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0))
service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO)
assertFalse(port.currentLiveIsViewerCreator == true)
assertFalse(port.schedulesIsViewerCreator == true)
@@ -173,7 +173,7 @@ class CreatorChannelHomeQueryServiceTest {
val service = createService(port, communityPort, preferenceService = preferenceService)
val viewer = createMember(id = 10L)
service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0))
service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO)
assertEquals(false, port.currentLiveCanViewAdultContent)
assertEquals(false, port.schedulesCanViewAdultContent)
@@ -191,7 +191,7 @@ class CreatorChannelHomeQueryServiceTest {
val service = createService(port)
val viewer = createMember(id = 10L)
val home = service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0))
val home = service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO)
assertEquals("donor", home.channelDonations.first().nickname)
}
@@ -206,7 +206,7 @@ class CreatorChannelHomeQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getHome(creatorId = 999L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0))
service.getHome(999L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO)
}
assertEquals("member.validation.user_not_found", exception.messageKey)
@@ -222,7 +222,7 @@ class CreatorChannelHomeQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0))
service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO)
}
assertEquals("member.validation.creator_not_found", exception.messageKey)
@@ -239,7 +239,7 @@ class CreatorChannelHomeQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0))
service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO)
}
assertNull(exception.messageKey)
@@ -256,7 +256,7 @@ class CreatorChannelHomeQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0))
service.getHome(1L, viewer, LocalDateTime.of(2026, 6, 13, 10, 0), Lang.KO)
}
assertNull(exception.messageKey)
@@ -408,6 +408,7 @@ class CreatorChannelHomeQueryServiceTest {
),
channelDonations = listOf(
CreatorChannelDonation(
memberId = 20L,
nickname = "fan",
profileImageUrl = "fan.png",
can = 50,
@@ -593,7 +594,7 @@ private class FakeCreatorChannelHomeQueryPort : CreatorChannelHomeQueryPort {
var seriesLocale: String? = null
var channelDonationNickname: String = "fan"
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCreatorRecord? = creator
override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelCreatorRecord? = creator
override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean = blocked
@@ -634,9 +635,11 @@ private class FakeCreatorChannelHomeQueryPort : CreatorChannelHomeQueryPort {
creatorId: Long,
viewerId: Long?,
now: LocalDateTime,
limit: Int
limit: Int,
lang: Lang
): List<CreatorChannelDonationRecord> = listOf(
CreatorChannelDonationRecord(
memberId = 20L,
nickname = channelDonationNickname,
profileImagePath = "profile/fan.png",
can = 30,
@@ -710,7 +713,7 @@ private class FakeCreatorChannelHomeQueryPort : CreatorChannelHomeQueryPort {
)
}
override fun findFanTalkSummary(creatorId: Long, viewerId: Long?): CreatorChannelFanTalkSummaryRecord {
override fun findFanTalkSummary(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelFanTalkSummaryRecord {
return CreatorChannelFanTalkSummaryRecord(
totalCount = 1,
latestFanTalk = CreatorChannelFanTalkRecord(
@@ -798,7 +801,7 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer
val homeCanViewAdultContentValues = mutableListOf<Boolean>()
val homeLimits = mutableListOf<Int>()
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCommunityCreatorRecord? {
override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelCommunityCreatorRecord? {
return CreatorChannelCommunityCreatorRecord(creatorId = creatorId, role = MemberRole.CREATOR, nickname = "creator")
}
@@ -811,7 +814,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer
viewerId: Long,
canViewAdultContent: Boolean,
offset: Long,
limit: Int
limit: Int,
lang: Lang
): List<CreatorChannelCommunityPostRecord> = emptyList()
override fun findHomeCommunityPosts(
@@ -819,7 +823,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer
viewerId: Long,
isPinned: Boolean,
canViewAdultContent: Boolean,
limit: Int
limit: Int,
lang: Lang
): List<CreatorChannelCommunityPostRecord> {
homeCreatorIds += creatorId
homeViewerIds += viewerId
@@ -850,7 +855,8 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer
override fun findCommunityPost(
postId: Long,
viewerId: Long,
canViewAdultContent: Boolean
canViewAdultContent: Boolean,
lang: Lang
): CreatorChannelCommunityPostRecord? = null
override fun countCommunityComments(postId: Long, viewerId: Long, creatorId: Long): Int = 0
@@ -860,12 +866,14 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer
viewerId: Long,
creatorId: Long,
offset: Long,
limit: Int
limit: Int,
lang: Lang
) = emptyList<kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityCommentRecord>()
override fun findLatestRepliesByCommentIds(
commentIds: List<Long>,
viewerId: Long
viewerId: Long,
lang: Lang
) = emptyList<kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityCommentRecord>()
override fun findRootCommentContext(
@@ -880,6 +888,7 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer
commentId: Long,
viewerId: Long,
offset: Long,
limit: Int
limit: Int,
lang: Lang
) = emptyList<kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityCommentRecord>()
}
@@ -9,6 +9,7 @@ import kr.co.vividnext.sodalive.content.order.OrderType
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.live.room.GenderRestriction
import kr.co.vividnext.sodalive.live.room.LiveRoom
import kr.co.vividnext.sodalive.member.Gender
@@ -274,7 +275,7 @@ class DefaultCreatorChannelLiveQueryRepositoryTest @Autowired constructor(
saveBlock(creator, viewer)
flushAndClear()
val record = repository.findCreator(creator.id!!, viewer.id!!)
val record = repository.findCreator(creator.id!!, viewer.id!!, Lang.KO)
assertEquals(creator.id, record!!.creatorId)
assertEquals(MemberRole.CREATOR, record.role)
@@ -2,7 +2,6 @@ package kr.co.vividnext.sodalive.v2.creator.channel.live.application
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.Gender
import kr.co.vividnext.sodalive.member.Member
@@ -42,7 +41,8 @@ class CreatorChannelLiveQueryServiceTest {
sort = ContentSort.LATEST,
page = 0,
size = 20,
now = now
now = now,
lang = Lang.KO
)
assertEquals(1L, port.findCreatorCreatorId)
@@ -75,7 +75,8 @@ class CreatorChannelLiveQueryServiceTest {
sort = ContentSort.LATEST,
page = 0,
size = 20,
now = LocalDateTime.of(2026, 6, 17, 10, 0)
now = LocalDateTime.of(2026, 6, 17, 10, 0),
lang = Lang.KO
)
assertTrue(port.currentLiveIsViewerCreator == true)
@@ -96,7 +97,8 @@ class CreatorChannelLiveQueryServiceTest {
sort = ContentSort.LATEST,
page = 0,
size = 20,
now = LocalDateTime.of(2026, 6, 17, 10, 0)
now = LocalDateTime.of(2026, 6, 17, 10, 0),
lang = Lang.KO
)
assertEquals(30, tab.liveReplayContentCount)
@@ -127,7 +129,8 @@ class CreatorChannelLiveQueryServiceTest {
sort = ContentSort.PRICE_LOW,
page = 2,
size = 20,
now = LocalDateTime.of(2026, 6, 17, 10, 0)
now = LocalDateTime.of(2026, 6, 17, 10, 0),
lang = Lang.KO
)
assertEquals(30, tab.liveReplayContentCount)
@@ -151,7 +154,7 @@ class CreatorChannelLiveQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getLiveTab(1L, viewer, ContentSort.LATEST, 0, 20, LocalDateTime.of(2026, 6, 17, 10, 0))
service.getLiveTab(1L, viewer, ContentSort.LATEST, 0, 20, LocalDateTime.of(2026, 6, 17, 10, 0), Lang.KO)
}
assertEquals("member.validation.user_not_found", exception.messageKey)
@@ -167,7 +170,7 @@ class CreatorChannelLiveQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getLiveTab(1L, viewer, ContentSort.LATEST, 0, 20, LocalDateTime.of(2026, 6, 17, 10, 0))
service.getLiveTab(1L, viewer, ContentSort.LATEST, 0, 20, LocalDateTime.of(2026, 6, 17, 10, 0), Lang.KO)
}
assertEquals("member.validation.creator_not_found", exception.messageKey)
@@ -184,7 +187,7 @@ class CreatorChannelLiveQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getLiveTab(1L, viewer, ContentSort.LATEST, 0, 20, LocalDateTime.of(2026, 6, 17, 10, 0))
service.getLiveTab(1L, viewer, ContentSort.LATEST, 0, 20, LocalDateTime.of(2026, 6, 17, 10, 0), Lang.KO)
}
assertNull(exception.messageKey)
@@ -198,7 +201,7 @@ class CreatorChannelLiveQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getLiveTab(1L, viewer, ContentSort.LATEST, -1, 20, LocalDateTime.of(2026, 6, 17, 10, 0))
service.getLiveTab(1L, viewer, ContentSort.LATEST, -1, 20, LocalDateTime.of(2026, 6, 17, 10, 0), Lang.KO)
}
assertEquals("common.error.invalid_request", exception.messageKey)
@@ -211,7 +214,7 @@ class CreatorChannelLiveQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getLiveTab(1L, viewer, ContentSort.LATEST, 0, 51, LocalDateTime.of(2026, 6, 17, 10, 0))
service.getLiveTab(1L, viewer, ContentSort.LATEST, 0, 51, LocalDateTime.of(2026, 6, 17, 10, 0), Lang.KO)
}
assertEquals("common.error.invalid_request", exception.messageKey)
@@ -240,14 +243,11 @@ class CreatorChannelLiveQueryServiceTest {
preferenceService.canViewAdultContent(Mockito.any(Member::class.java) ?: createMember(id = 0L))
).thenReturn(canViewAdultContent)
val messageSource = SodaMessageSource()
val langContext = LangContext()
langContext.setLang(Lang.KO)
return CreatorChannelLiveQueryService(
queryPortProvider = portProvider,
queryPolicy = CreatorChannelLiveReplayQueryPolicy(),
memberContentPreferenceService = preferenceService,
messageSource = messageSource,
langContext = langContext,
cloudFrontHost = "https://cdn.test"
)
}
@@ -337,7 +337,7 @@ private class FakeCreatorChannelLiveQueryPort : CreatorChannelLiveQueryPort {
var listOffset: Long? = null
var listLimit: Int? = null
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCreatorRecord? {
override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelCreatorRecord? {
findCreatorCreatorId = creatorId
findCreatorViewerId = viewerId
return creator
@@ -13,6 +13,7 @@ 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.creator.admin.content.series.SeriesPublishedDaysOfWeek
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesState
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
@@ -50,8 +51,8 @@ class DefaultCreatorChannelSeriesQueryRepositoryTest @Autowired constructor(
saveBlock(creator, viewer)
flushAndClear()
val record = repository.findCreator(creator.id!!, viewer.id!!)
val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!)
val record = repository.findCreator(creator.id!!, viewer.id!!, Lang.KO)
val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!, Lang.KO)
assertEquals(creator.id, record!!.creatorId)
assertEquals(MemberRole.CREATOR, record.role)
@@ -42,7 +42,8 @@ class CreatorChannelSeriesQueryServiceTest {
sort = "UNKNOWN",
page = -1,
size = 100,
now = now
now = now,
lang = Lang.EN
)
assertEquals(ContentSort.LATEST, tab.sort)
@@ -70,7 +71,7 @@ class CreatorChannelSeriesQueryServiceTest {
val service = createService(port)
val viewer = createMember(id = 1L)
val tab = service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0))
val tab = service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0), Lang.EN)
assertNull(tab.series.first().purchasedContentCount)
assertNull(tab.series.first().paidContentCount)
@@ -86,7 +87,7 @@ class CreatorChannelSeriesQueryServiceTest {
val service = createService(port)
val viewer = createMember(id = 10L)
val tab = service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0))
val tab = service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0), Lang.EN)
assertNull(tab.series.first().coverImageUrl)
assertEquals(0, tab.series.first().purchasedPaidContentRate)
@@ -100,7 +101,7 @@ class CreatorChannelSeriesQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0))
service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0), Lang.EN)
}
assertEquals("member.validation.user_not_found", exception.messageKey)
@@ -114,7 +115,7 @@ class CreatorChannelSeriesQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0))
service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0), Lang.EN)
}
assertEquals("member.validation.creator_not_found", exception.messageKey)
@@ -128,7 +129,7 @@ class CreatorChannelSeriesQueryServiceTest {
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0))
service.getSeriesTab(1L, viewer, null, null, null, LocalDateTime.of(2026, 6, 20, 10, 0), Lang.EN)
}
assertNull(exception.messageKey)
@@ -192,7 +193,7 @@ private class FakeCreatorChannelSeriesQueryPort : CreatorChannelSeriesQueryPort
var listLimit: Int? = null
var listCanViewAdultContent: Boolean? = null
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelSeriesCreatorRecord? = creator
override fun findCreator(creatorId: Long, viewerId: Long?, lang: Lang): CreatorChannelSeriesCreatorRecord? = creator
override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean = blocked
override fun countSeries(creatorId: Long, now: LocalDateTime, canViewAdultContent: Boolean): Int = seriesCount
@@ -2,8 +2,11 @@ package kr.co.vividnext.sodalive.v2.gift.application
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.support.LegacyCreatorNameTestConfiguration
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.Gift
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategory
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository
@@ -35,14 +38,15 @@ import java.time.LocalDateTime
"spring.datasource.url=jdbc:h2:mem:gift-query;MODE=MySQL;NON_KEYWORDS=VALUE"
]
)
@Import(QueryDslConfig::class)
@Import(QueryDslConfig::class, LegacyCreatorNameTestConfiguration::class)
class GiftQueryServiceTest @Autowired constructor(
private val categoryRepository: GiftCategoryRepository,
private val sizePriceRepository: GiftSizePriceRepository,
private val giftRepository: GiftRepository,
private val deliveryRepository: GiftDeliveryRepository,
private val memberRepository: MemberRepository,
private val giftMailboxRepository: GiftMailboxRepository
private val giftMailboxRepository: GiftMailboxRepository,
private val creatorLocalizedNameService: CreatorLocalizedNameService
) {
@Test
fun `폼 옵션은 활성 카테고리와 활성 사이즈 가격만 반환한다`() {
@@ -79,7 +83,8 @@ class GiftQueryServiceTest @Autowired constructor(
memberId = sender.id!!,
type = GiftListType.SENT,
page = 0,
size = 20
size = 20,
lang = Lang.KO
)
assertEquals(1L, result.totalCount)
@@ -122,7 +127,8 @@ class GiftQueryServiceTest @Autowired constructor(
memberId = recipient.id!!,
type = GiftListType.RECEIVED,
page = 0,
size = 20
size = 20,
lang = Lang.KO
)
assertEquals(1L, result.totalCount)
@@ -162,7 +168,8 @@ class GiftQueryServiceTest @Autowired constructor(
memberId = member.id!!,
type = GiftListType.ALL,
page = 0,
size = 20
size = 20,
lang = Lang.KO
)
assertEquals(2L, result.totalCount)
@@ -220,7 +227,8 @@ class GiftQueryServiceTest @Autowired constructor(
memberId = member.id!!,
type = GiftListType.ALL,
page = 0,
size = 20
size = 20,
lang = Lang.KO
)
val items = result.items.associateBy { it.applicationNo }
@@ -245,7 +253,8 @@ class GiftQueryServiceTest @Autowired constructor(
memberId = sender.id!!,
type = GiftListType.SENT,
page = -1,
size = 0
size = 0,
lang = Lang.KO
)
assertEquals(0, result.page)
@@ -272,7 +281,7 @@ class GiftQueryServiceTest @Autowired constructor(
delivery.recipientAddress = "서울시 중구"
delivery.recipientAddressDetail = "202호"
val result = queryService().getGiftDetail(sender.id!!, gift.applicationNo)
val result = queryService().getGiftDetail(sender.id!!, gift.applicationNo, Lang.KO)
assertEquals(GiftDirection.SENT, result.direction)
assertEquals(recipient.nickname, result.giftInfo.recipientCreatorNickname)
@@ -335,7 +344,7 @@ class GiftQueryServiceTest @Autowired constructor(
)
saveDelivery(gift).applyStatusTimes(status)
val result = service.getGiftDetail(sender.id!!, gift.applicationNo)
val result = service.getGiftDetail(sender.id!!, gift.applicationNo, Lang.KO)
assertEquals("소다라이브 선물 담당자", result.mailbox?.name)
assertEquals("01012345678", result.mailbox?.phoneNumber)
@@ -370,7 +379,7 @@ class GiftQueryServiceTest @Autowired constructor(
)
saveDelivery(gift).applyStatusTimes(status)
val result = service.getGiftDetail(sender.id!!, gift.applicationNo)
val result = service.getGiftDetail(sender.id!!, gift.applicationNo, Lang.KO)
assertNull(result.mailbox)
}
@@ -383,7 +392,7 @@ class GiftQueryServiceTest @Autowired constructor(
)
saveDelivery(receivedGift).applyStatusTimes(GiftStatus.TRACKING_REGISTERED)
val recipientResult = service.getGiftDetail(recipient.id!!, receivedGift.applicationNo)
val recipientResult = service.getGiftDetail(recipient.id!!, receivedGift.applicationNo, Lang.KO)
assertNull(recipientResult.mailbox)
}
@@ -401,7 +410,7 @@ class GiftQueryServiceTest @Autowired constructor(
)
saveDelivery(gift).applyStatusTimes(GiftStatus.INSPECTION_COMPLETED)
val result = queryService().getGiftDetail(sender.id!!, gift.applicationNo)
val result = queryService().getGiftDetail(sender.id!!, gift.applicationNo, Lang.KO)
assertNull(result.mailbox)
}
@@ -426,7 +435,7 @@ class GiftQueryServiceTest @Autowired constructor(
recipientAddressDetail = "202호"
}
val result = queryService().getGiftDetail(recipient.id!!, gift.applicationNo)
val result = queryService().getGiftDetail(recipient.id!!, gift.applicationNo, Lang.KO)
assertEquals(GiftDirection.RECEIVED, result.direction)
assertNull(result.giftInfo.recipientCreatorNickname)
@@ -478,13 +487,13 @@ class GiftQueryServiceTest @Autowired constructor(
val service = queryService()
val hiddenException = assertThrows(SodaException::class.java) {
service.getGiftDetail(recipient.id!!, hiddenGift.applicationNo)
service.getGiftDetail(recipient.id!!, hiddenGift.applicationNo, Lang.KO)
}
val unrelatedException = assertThrows(SodaException::class.java) {
service.getGiftDetail(other.id!!, visibleGift.applicationNo)
service.getGiftDetail(other.id!!, visibleGift.applicationNo, Lang.KO)
}
val missingDeliveryException = assertThrows(SodaException::class.java) {
service.getGiftDetail(sender.id!!, missingDeliveryGift.applicationNo)
service.getGiftDetail(sender.id!!, missingDeliveryGift.applicationNo, Lang.KO)
}
assertEquals("common.error.invalid_request", hiddenException.messageKey)
@@ -517,8 +526,8 @@ class GiftQueryServiceTest @Autowired constructor(
}
val service = queryService()
val pendingResult = service.getGiftDetail(recipient.id!!, pendingGift.applicationNo)
val terminalResult = service.getGiftDetail(recipient.id!!, terminalGift.applicationNo)
val pendingResult = service.getGiftDetail(recipient.id!!, pendingGift.applicationNo, Lang.KO)
val terminalResult = service.getGiftDetail(recipient.id!!, terminalGift.applicationNo, Lang.KO)
assertEquals(true, pendingResult.recipientAddressRequired)
assertNull(pendingResult.recipientAddress)
@@ -551,8 +560,8 @@ class GiftQueryServiceTest @Autowired constructor(
}
val service = queryService()
val canceledResult = service.getGiftDetail(sender.id!!, canceledGift.applicationNo)
val undeliverableResult = service.getGiftDetail(sender.id!!, undeliverableGift.applicationNo)
val canceledResult = service.getGiftDetail(sender.id!!, canceledGift.applicationNo, Lang.KO)
val undeliverableResult = service.getGiftDetail(sender.id!!, undeliverableGift.applicationNo, Lang.KO)
assertEquals(NOW.plusHours(1), canceledResult.delivery.canceledAt)
assertNull(canceledResult.statusTimeline[1].occurredAt)
@@ -639,7 +648,8 @@ class GiftQueryServiceTest @Autowired constructor(
deliveryRepository,
memberRepository,
giftMailboxRepository,
"https://cdn.test"
"https://cdn.test",
creatorLocalizedNameService
)
private companion object {
@@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.CreatorCommunityComment
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.CreatorCommunityLike
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.live.room.GenderRestriction
import kr.co.vividnext.sodalive.live.room.LiveRoom
import kr.co.vividnext.sodalive.member.Gender
@@ -62,7 +63,7 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
activeFollow.createdAt = LocalDateTime.of(2026, 6, 25, 0, 0)
flushAndClear()
val creators = repository.findFollowingCreators(memberId = viewer.id!!, limit = 20)
val creators = repository.findFollowingCreators(memberId = viewer.id!!, limit = 20, lang = Lang.KO)
assertEquals(listOf(olderCreator.id!!, activeCreator.id!!), creators.map { it.creatorId })
assertEquals("https://cdn.test/active.png", creators.last().creatorProfileImageUrl)
@@ -82,7 +83,7 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
saveBlock(creatorBlockedViewer, viewer)
flushAndClear()
val creators = repository.findFollowingCreators(memberId = viewer.id!!, limit = 20)
val creators = repository.findFollowingCreators(memberId = viewer.id!!, limit = 20, lang = Lang.KO)
assertEquals(listOf(visibleCreator.id!!), creators.map { it.creatorId })
}
@@ -110,7 +111,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
canViewAdultContent = false,
effectiveViewerGender = Gender.NONE,
isViewerCreator = false,
limit = 10
limit = 10,
lang = Lang.KO
)
assertEquals(listOf(latest.id!!, older.id!!), lives.map { it.liveId })
@@ -143,7 +145,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
canViewAdultContent = true,
effectiveViewerGender = Gender.FEMALE,
isViewerCreator = true,
limit = 10
limit = 10,
lang = Lang.KO
)
assertEquals(listOf(visible.id!!), lives.map { it.liveId })
@@ -175,7 +178,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
effectiveViewerGender = Gender.NONE,
isViewerCreator = false,
now = LocalDateTime.of(2026, 6, 25, 12, 0),
limit = 3
limit = 3,
lang = Lang.KO
)
assertEquals(listOf("LIVE:${live.id!!}", "AUDIO:${audio.id!!}"), schedules.map { it.scheduleId })
@@ -210,7 +214,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
effectiveViewerGender = Gender.NONE,
isViewerCreator = false,
now = LocalDateTime.of(2026, 6, 25, 0, 0),
limit = 3
limit = 3,
lang = Lang.KO
)
assertEquals(listOf("AUDIO:${scheduledAudio.id!!}"), schedules.map { it.scheduleId })
@@ -233,7 +238,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
effectiveViewerGender = Gender.NONE,
isViewerCreator = false,
now = LocalDateTime.of(2026, 6, 25, 14, 30),
limit = 3
limit = 3,
lang = Lang.KO
)
assertTrue(schedules.map { it.scheduleId }.contains("AUDIO:${sameKstDayEvening.id!!}"))
@@ -259,7 +265,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
effectiveViewerGender = Gender.NONE,
isViewerCreator = false,
now = LocalDateTime.of(2026, 6, 25, 0, 0),
limit = 10
limit = 10,
lang = Lang.KO
)
assertEquals(
@@ -302,7 +309,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
effectiveViewerGender = Gender.FEMALE,
isViewerCreator = true,
now = LocalDateTime.of(2026, 6, 25, 0, 0),
limit = 10
limit = 10,
lang = Lang.KO
)
assertEquals(listOf("LIVE:${visibleLive.id!!}", "AUDIO:${audio.id!!}"), schedules.map { it.scheduleId })
@@ -332,7 +340,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
canViewAdultContent = false,
nowUtc = LocalDateTime.of(2026, 6, 25, 9, 30),
limit = 30
limit = 30,
lang = Lang.KO
)
assertEquals(listOf(latestVisible.id!!.toString()), news.map { it.newsId })
@@ -377,13 +386,15 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
canViewAdultContent = true,
nowUtc = LocalDateTime.of(2026, 6, 14, 23, 59),
limit = 30
limit = 30,
lang = Lang.KO
)
val afterLatestVisible = repository.findRecentNews(
memberId = viewer.id!!,
canViewAdultContent = true,
nowUtc = LocalDateTime.of(2026, 6, 15, 0, 0),
limit = 30
limit = 30,
lang = Lang.KO
)
assertEquals(listOf(oldSecondNews.id!!.toString(), oldFirstNews.id!!.toString()), beforeLatestVisible.map { it.newsId })
@@ -412,7 +423,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
canViewAdultContent = true,
nowUtc = LocalDateTime.of(2026, 6, 15, 0, 0),
limit = 30
limit = 30,
lang = Lang.KO
)
assertTrue(news.none { it.type == FollowingNewsType.CREATOR_RANKING })
@@ -475,7 +487,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
canViewAdultContent = true,
nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0),
limit = 2
limit = 2,
lang = Lang.KO
)
assertEquals(listOf(latestFirst.id!!.toString(), latestSecond.id!!.toString()), news.map { it.newsId })
@@ -516,7 +529,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
canViewAdultContent = true,
nowUtc = LocalDateTime.of(2026, 6, 25, 11, 0),
limit = 30
limit = 30,
lang = Lang.KO
)
assertEquals(listOf(visible.id!!.toString()), news.map { it.newsId })
@@ -556,7 +570,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
canViewAdultContent = false,
nowUtc = LocalDateTime.of(2026, 6, 25, 11, 0),
limit = 30
limit = 30,
lang = Lang.KO
)
assertEquals(listOf(visible.id!!.toString()), news.map { it.newsId })
@@ -577,7 +592,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
canViewAdultContent = false,
nowUtc = LocalDateTime.of(2026, 6, 25, 14, 30),
limit = 30
limit = 30,
lang = Lang.KO
)
assertEquals(listOf(visibleNow.id!!.toString()), news.map { it.newsId })
@@ -628,7 +644,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
canViewAdultContent = true,
nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0),
limit = 30
limit = 30,
lang = Lang.KO
)
val communityNews = news.first { it.type == FollowingNewsType.COMMUNITY_POST }.communityPost!!
@@ -668,7 +685,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
canViewAdultContent = true,
nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0),
limit = 30
limit = 30,
lang = Lang.KO
)
assertTrue(news.isEmpty())
@@ -713,7 +731,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
canViewAdultContent = true,
nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0),
limit = 30
limit = 30,
lang = Lang.KO
)
assertEquals(listOf(negativeFreePost.id!!, freePost.id!!), news.map { it.communityPost?.postId })
@@ -759,13 +778,15 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
canViewAdultContent = false,
nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0),
limit = 30
limit = 30,
lang = Lang.KO
)
val visibleNews = repository.findRecentNews(
memberId = viewer.id!!,
canViewAdultContent = true,
nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0),
limit = 30
limit = 30,
lang = Lang.KO
)
assertEquals(emptyList<Long>(), hiddenNews.map { it.communityPost?.postId ?: it.audioContent?.contentId })
@@ -824,7 +845,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
canViewAdultContent = true,
nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0),
limit = 30
limit = 30,
lang = Lang.KO
)
assertEquals(
@@ -1,7 +1,6 @@
package kr.co.vividnext.sodalive.v2.home.following.application
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
@@ -29,13 +28,11 @@ import java.util.TimeZone
class HomeFollowingQueryServiceTest {
private val queryPort = RecordingHomeFollowingQueryPort()
private val memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
private val langContext = LangContext()
private val creatorCommunityTranslationService = Mockito.mock(CreatorCommunityTranslationService::class.java)
private val now = LocalDateTime.of(2026, 6, 25, 12, 0)
private val service = HomeFollowingQueryService(
queryPort,
memberContentPreferenceService,
langContext,
creatorCommunityTranslationService
) { now }
@@ -45,7 +42,7 @@ class HomeFollowingQueryServiceTest {
val member = member(10L)
Mockito.`when`(memberContentPreferenceService.canViewAdultContent(member)).thenReturn(true)
val home = service.findHomeFollowing(member)
val home = service.findHomeFollowing(member, Lang.KO)
assertEquals(listOf(HomeFollowingCreator(1L, "creator", "profile")), home.followingCreators)
assertEquals(emptyList<Any>(), home.recentChats)
@@ -63,7 +60,7 @@ class HomeFollowingQueryServiceTest {
val member = member(11L)
Mockito.`when`(memberContentPreferenceService.canViewAdultContent(member)).thenReturn(false)
service.findHomeFollowing(member)
service.findHomeFollowing(member, Lang.KO)
assertEquals(false, queryPort.onAirCanViewAdultContent)
assertEquals(false, queryPort.monthlySchedulesCanViewAdultContent)
@@ -81,7 +78,7 @@ class HomeFollowingQueryServiceTest {
}
Mockito.`when`(memberContentPreferenceService.canViewAdultContent(member)).thenReturn(true)
service.findHomeFollowing(member)
service.findHomeFollowing(member, Lang.KO)
assertEquals(Gender.FEMALE, queryPort.onAirEffectiveViewerGender)
assertEquals(Gender.FEMALE, queryPort.monthlySchedulesEffectiveViewerGender)
@@ -99,14 +96,13 @@ class HomeFollowingQueryServiceTest {
val defaultService = HomeFollowingQueryService(
defaultQueryPort,
memberContentPreferenceService,
langContext,
creatorCommunityTranslationService
)
val member = member(12L)
Mockito.`when`(memberContentPreferenceService.canViewAdultContent(member)).thenReturn(true)
val beforeUtc = LocalDateTime.now(ZoneOffset.UTC).minusSeconds(1)
defaultService.findHomeFollowing(member)
defaultService.findHomeFollowing(member, Lang.KO)
val afterUtc = LocalDateTime.now(ZoneOffset.UTC).plusSeconds(1)
val capturedNow = defaultQueryPort.recentNewsNow!!
@@ -121,9 +117,9 @@ class HomeFollowingQueryServiceTest {
@Test
@DisplayName("팔로잉 최근 소식은 커뮤니티 게시물만 영어 표시 본문으로 일괄 치환하고 다른 소식과 원문 폴백을 유지한다")
fun shouldTranslateOnlyCommunityPostNewsInOneEnglishBatch() {
langContext.setLang(Lang.EN)
val translatedNews = communityNews("community-1", 101L, "original-1")
val audioNews = HomeFollowingNews(
creatorMemberId = 10L,
newsId = "audio-1",
type = FollowingNewsType.AUDIO_CONTENT,
visibleFromAtUtc = "2026-06-25T11:59:00Z"
@@ -138,7 +134,7 @@ class HomeFollowingQueryServiceTest {
)
)
val home = service.findHomeFollowing(member(14L))
val home = service.findHomeFollowing(member(14L), Lang.EN)
assertEquals("translated-1-en", home.recentNews[0].communityPost?.content)
assertEquals(audioNews, home.recentNews[1])
@@ -153,6 +149,7 @@ class HomeFollowingQueryServiceTest {
private fun communityNews(newsId: String, postId: Long, content: String): HomeFollowingNews {
return HomeFollowingNews(
creatorMemberId = 10L,
newsId = newsId,
type = FollowingNewsType.COMMUNITY_POST,
visibleFromAtUtc = "2026-06-25T12:00:00Z",
@@ -186,7 +183,7 @@ class HomeFollowingQueryServiceTest {
var recentNewsCanViewAdultContent: Boolean? = null
var recentNews: List<HomeFollowingNews> = emptyList()
override fun findFollowingCreators(memberId: Long, limit: Int): List<HomeFollowingCreator> {
override fun findFollowingCreators(memberId: Long, limit: Int, lang: Lang): List<HomeFollowingCreator> {
this.memberId = memberId
followingCreatorsLimit = limit
return listOf(HomeFollowingCreator(1L, "creator", "profile"))
@@ -197,7 +194,8 @@ class HomeFollowingQueryServiceTest {
canViewAdultContent: Boolean,
effectiveViewerGender: Gender,
isViewerCreator: Boolean,
limit: Int
limit: Int,
lang: Lang
): List<HomeFollowingLive> {
onAirCanViewAdultContent = canViewAdultContent
onAirEffectiveViewerGender = effectiveViewerGender
@@ -212,7 +210,8 @@ class HomeFollowingQueryServiceTest {
effectiveViewerGender: Gender,
isViewerCreator: Boolean,
now: LocalDateTime,
limit: Int
limit: Int,
lang: Lang
): List<HomeFollowingSchedule> {
monthlySchedulesCanViewAdultContent = canViewAdultContent
monthlySchedulesEffectiveViewerGender = effectiveViewerGender
@@ -226,7 +225,8 @@ class HomeFollowingQueryServiceTest {
memberId: Long,
canViewAdultContent: Boolean,
nowUtc: LocalDateTime,
limit: Int
limit: Int,
lang: Lang
): List<HomeFollowingNews> {
recentNewsCanViewAdultContent = canViewAdultContent
recentNewsNow = nowUtc
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScoreSpec
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotRecord
@@ -98,7 +99,7 @@ class DefaultCreatorRankingSnapshotRepositoryTest @Autowired constructor(
)
)
val latestVisible = adapter.findLatestVisibleSnapshots(CreatorRankingType.WEEKLY, NOW)
val latestVisible = adapter.findLatestVisibleSnapshots(CreatorRankingType.WEEKLY, NOW, Lang.KO)
assertEquals(listOf(3L, 4L, 2L), latestVisible.map { it.creatorId })
assertEquals(listOf(1, 2, 3), latestVisible.map { it.rankNo })
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.ranking.application
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingBlockPort
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotPort
@@ -19,7 +20,11 @@ class CreatorRankingQueryServiceTest {
@Test
@DisplayName("fallback refresh 후 재조회가 같은 read-only transaction에 묶이지 않도록 조회 메서드에는 Transactional이 없다")
fun shouldNotWrapQueryMethodWithTransactional() {
val method = CreatorRankingQueryService::class.java.getDeclaredMethod("getCreatorRankings", java.lang.Long::class.java)
val method = CreatorRankingQueryService::class.java.getDeclaredMethod(
"getCreatorRankings",
java.lang.Long::class.java,
Lang::class.java
)
assertFalse(method.isAnnotationPresent(Transactional::class.java))
}
@@ -32,7 +37,7 @@ class CreatorRankingQueryServiceTest {
snapshotPort.latestResponses = ArrayDeque(listOf(emptyList(), listOf(snapshot(1, rankNo = 1))))
val service = service(snapshotPort = snapshotPort, snapshotJobService = jobService)
val result = service.getCreatorRankings(viewerMemberId = null)
val result = service.getCreatorRankings(viewerMemberId = null, lang = Lang.KO)
Mockito.verify(jobService).refreshLastCompletedWeekByFallback()
assertEquals(2, snapshotPort.latestCallCount)
@@ -48,7 +53,7 @@ class CreatorRankingQueryServiceTest {
Mockito.doThrow(IllegalStateException("fallback failed")).`when`(jobService).refreshLastCompletedWeekByFallback()
val service = service(snapshotPort = snapshotPort, snapshotJobService = jobService)
val result = service.getCreatorRankings(viewerMemberId = null)
val result = service.getCreatorRankings(viewerMemberId = null, lang = Lang.KO)
assertFalse(result.showRankChange)
assertTrue(result.items.isEmpty())
@@ -66,7 +71,7 @@ class CreatorRankingQueryServiceTest {
snapshotPort.previousSnapshots = listOf(snapshot(1, rankNo = 1), snapshot(2, rankNo = 2))
val service = service(snapshotPort = snapshotPort, snapshotJobService = jobService)
val result = service.getCreatorRankings(viewerMemberId = null)
val result = service.getCreatorRankings(viewerMemberId = null, lang = Lang.KO)
Mockito.verify(jobService, Mockito.never()).refreshLastCompletedWeekByFallback()
assertTrue(result.showRankChange)
@@ -86,7 +91,7 @@ class CreatorRankingQueryServiceTest {
blockPort.blockedCreatorIds = setOf(2L)
val service = service(snapshotPort = snapshotPort, blockPort = blockPort)
val result = service.getCreatorRankings(viewerMemberId = 99L)
val result = service.getCreatorRankings(viewerMemberId = 99L, lang = Lang.KO)
assertEquals(listOf(1, 2), result.items.map { it.rank })
assertEquals(0L, result.items[1].creatorId)
@@ -133,7 +138,8 @@ private class FakeCreatorRankingQuerySnapshotPort : CreatorRankingSnapshotPort {
override fun findLatestVisibleSnapshots(
rankingType: CreatorRankingType,
nowUtc: LocalDateTime
nowUtc: LocalDateTime,
lang: Lang
): List<CreatorRankingSnapshotRecord> {
latestCallCount++
return if (latestResponses.size > 1) {
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.ranking.application
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScoreSpec
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingSnapshotCandidate
@@ -120,7 +121,7 @@ private class FakeCreatorRankingSnapshotPort : CreatorRankingSnapshotPort {
) = snapshots
override fun findLatestSnapshots() = snapshots
override fun findPreviousCompletedSnapshots() = snapshots
override fun findLatestVisibleSnapshots(rankingType: CreatorRankingType, nowUtc: LocalDateTime) = snapshots
override fun findLatestVisibleSnapshots(rankingType: CreatorRankingType, nowUtc: LocalDateTime, lang: Lang) = snapshots
override fun findPreviousVisibleSnapshots(
rankingType: CreatorRankingType,
currentAggregationStartAtUtc: LocalDateTime,
@@ -89,7 +89,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
}
flushAndClear()
val lives = repository.findLiveRecommendations(limit = 20, includeAdultLives = false)
val lives = repository.findLiveRecommendations(limit = 20, includeAdultLives = false, lang = Lang.KO)
assertEquals(20, lives.size)
assertEquals(true, lives.zipWithNext().all { it.first.liveRoomId > it.second.liveRoomId })
@@ -109,8 +109,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
val oldest = saveLiveRoom(creator, baseAt, channelName = "paged-live-oldest", isAdult = false)
flushAndClear()
val page0 = repository.findLiveRecommendations(offset = 0L, limit = 3, includeAdultLives = false)
val page1 = repository.findLiveRecommendations(offset = 2L, limit = 3, includeAdultLives = false)
val page0 = repository.findLiveRecommendations(offset = 0L, limit = 3, includeAdultLives = false, lang = Lang.KO)
val page1 = repository.findLiveRecommendations(offset = 2L, limit = 3, includeAdultLives = false, lang = Lang.KO)
assertEquals(listOf(newest.id, middle.id, oldest.id), page0.map { it.liveRoomId })
assertEquals(listOf(oldest.id), page1.map { it.liveRoomId })
@@ -135,7 +135,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
offset = 0,
limit = 1,
memberId = viewer.id,
includeAdultLives = true
includeAdultLives = true,
lang = Lang.KO
)
assertEquals(listOf(live.id), lives.map { it.liveRoomId })
@@ -170,7 +171,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
offset = 0,
limit = 10,
memberId = viewer.id,
includeAdultLives = false
includeAdultLives = false,
lang = Lang.KO
)
assertEquals(listOf(newerVisibleLive.id, olderVisibleLive.id), lives.map { it.liveRoomId })
@@ -191,7 +193,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
saveBlock(creatorBlockedViewer, viewer)
flushAndClear()
val lives = repository.findLiveRecommendations(limit = 10, memberId = viewer.id)
val lives = repository.findLiveRecommendations(limit = 10, memberId = viewer.id, lang = Lang.KO)
assertEquals(listOf(visibleLive.id), lives.map { it.liveRoomId })
}
@@ -221,7 +223,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
limit = 10,
memberId = viewer.id,
isViewerCreator = true,
effectiveViewerGender = Gender.NONE
effectiveViewerGender = Gender.NONE,
lang = Lang.KO
)
assertEquals(listOf(ownerLive.id, availableLive.id), lives.map { it.liveRoomId })
@@ -244,7 +247,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
limit = 10,
memberId = viewer.id,
isViewerCreator = false,
effectiveViewerGender = Gender.NONE
effectiveViewerGender = Gender.NONE,
lang = Lang.KO
)
assertEquals(listOf(restrictedLive.id), lives.map { it.liveRoomId })
@@ -281,7 +285,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
limit = 10,
memberId = viewer.id,
isViewerCreator = true,
effectiveViewerGender = Gender.MALE
effectiveViewerGender = Gender.MALE,
lang = Lang.KO
)
assertEquals(listOf(ownerLive.id, allLive.id, maleOnlyLive.id), lives.map { it.liveRoomId })
@@ -311,7 +316,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
limit = 10,
memberId = viewer.id,
isViewerCreator = true,
effectiveViewerGender = Gender.FEMALE
effectiveViewerGender = Gender.FEMALE,
lang = Lang.KO
)
assertEquals(listOf(ownerLive.id, femaleOnlyLive.id), lives.map { it.liveRoomId })
@@ -341,7 +347,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
val lives = repository.findLiveRecommendations(
limit = 10,
memberId = viewer.id,
effectiveViewerGender = Gender.FEMALE
effectiveViewerGender = Gender.FEMALE,
lang = Lang.KO
)
assertEquals(listOf(femaleOnlyLive.id, allLive.id), lives.map { it.liveRoomId })
@@ -371,12 +378,14 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
val noneGenderLives = repository.findLiveRecommendations(
limit = 10,
memberId = viewer.id,
effectiveViewerGender = Gender.NONE
effectiveViewerGender = Gender.NONE,
lang = Lang.KO
)
val nullGenderLives = repository.findLiveRecommendations(
limit = 10,
memberId = viewer.id,
effectiveViewerGender = null
effectiveViewerGender = null,
lang = Lang.KO
)
val expectedIds = listOf(allLive.id, femaleOnlyLive.id, maleOnlyLive.id)
@@ -659,7 +668,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
updateCreatedAt("CreatorCommunity", community.id!!, baseAt.minusHours(3))
flushAndClear()
val creators = repository.findRecentlyActiveCreators(limit = 10)
val creators = repository.findRecentlyActiveCreators(limit = 10, lang = Lang.KO)
val byCreatorNickname = creators.associateBy { it.creatorNickname }
assertEquals(4, creators.size)
@@ -699,7 +708,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
saveLiveRoom(emptyChannelCreator, baseAt, channelName = "", isActive = false)
flushAndClear()
val creators = repository.findRecentlyActiveCreators(limit = 10)
val creators = repository.findRecentlyActiveCreators(limit = 10, lang = Lang.KO)
assertEquals(listOf(inactiveLiveCreator.nickname), creators.map { it.creatorNickname })
assertEquals(inactiveLiveCreator.id, creators.single().creatorId)
@@ -723,8 +732,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
updateCreatedAt("CreatorCommunity", adultCommunity.id!!, baseAt)
flushAndClear()
val hiddenCreators = repository.findRecentlyActiveCreators(limit = 10, includeAdultActivities = false)
val visibleCreators = repository.findRecentlyActiveCreators(limit = 10, includeAdultActivities = true)
val hiddenCreators = repository.findRecentlyActiveCreators(limit = 10, includeAdultActivities = false, lang = Lang.KO)
val visibleCreators = repository.findRecentlyActiveCreators(limit = 10, includeAdultActivities = true, lang = Lang.KO)
assertEquals(listOf(normalLiveCreator.nickname), hiddenCreators.map { it.creatorNickname })
assertEquals(
@@ -762,7 +771,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
saveBlock(creatorBlockedViewer, viewer)
flushAndClear()
val creators = repository.findRecentlyActiveCreators(limit = 10, memberId = viewer.id)
val creators = repository.findRecentlyActiveCreators(limit = 10, memberId = viewer.id, lang = Lang.KO)
assertEquals(listOf(visibleCreator.nickname), creators.map { it.creatorNickname })
assertEquals(CreatorActivityType.COMMUNITY, creators.single().activityType)
@@ -779,7 +788,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
updateCreatedAt("CreatorCommunity", community.id!!, baseAt.plusMinutes(1))
flushAndClear()
val creators = repository.findRecentlyActiveCreators(limit = 10)
val creators = repository.findRecentlyActiveCreators(limit = 10, lang = Lang.KO)
assertEquals(listOf(visibleCreator.nickname), creators.map { it.creatorNickname })
assertEquals(CreatorActivityType.COMMUNITY, creators.single().activityType)
@@ -1597,7 +1606,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
updateCreatedAt("Member", newHighScoreCreator.id!!, now.minusDays(60))
flushAndClear()
val creators = repository.findRecentDebutCreators(now, limit = 10)
val creators = repository.findRecentDebutCreators(now, limit = 10, lang = Lang.KO)
assertEquals(listOf(newHighScoreCreator.id, newLowScoreCreator.id), creators.map { it.creatorId })
}
@@ -1623,7 +1632,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
)
flushAndClear()
val creators = repository.findRecentDebutCreators(now, limit = 10)
val creators = repository.findRecentDebutCreators(now, limit = 10, lang = Lang.KO)
assertEquals(listOf(endedLiveCreator.id), creators.map { it.creatorId })
}
@@ -1647,7 +1656,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
updateCreatedAt("AudioContentLike", baseLike2.id!!, now.minusDays(1))
flushAndClear()
val creators = repository.findRecentDebutCreators(now, limit = 2)
val creators = repository.findRecentDebutCreators(now, limit = 2, lang = Lang.KO)
assertEquals(listOf(boostedCreator.id, baseCreator.id), creators.map { it.creatorId })
}
@@ -1671,8 +1680,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
updateCreatedAt("AudioContentLike", like.id!!, now.minusHours(1))
flushAndClear()
val page0 = repository.findRecentDebutCreators(now, offset = 0L, limit = 2, includeAdultContents = false)
val page1 = repository.findRecentDebutCreators(now, offset = 1L, limit = 2, includeAdultContents = false)
val page0 = repository.findRecentDebutCreators(now, offset = 0L, limit = 2, includeAdultContents = false, lang = Lang.KO)
val page1 = repository.findRecentDebutCreators(now, offset = 1L, limit = 2, includeAdultContents = false, lang = Lang.KO)
assertEquals(listOf(normalNewest.id, normalOldest.id), page0.map { it.creatorId })
assertEquals(listOf(normalOldest.id), page1.map { it.creatorId })
@@ -1693,7 +1702,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
saveBlock(creatorBlockedViewer, viewer)
flushAndClear()
val creators = repository.findRecentDebutCreators(now, limit = 10, memberId = viewer.id)
val creators = repository.findRecentDebutCreators(now, limit = 10, memberId = viewer.id, lang = Lang.KO)
assertEquals(listOf(visibleCreator.id), creators.map { it.creatorId })
}
@@ -1710,9 +1719,9 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
saveAudioContent(creator3, now.minusDays(5), isActive = true)
flushAndClear()
val page0 = repository.findRecentDebutCreators(now, offset = 0L, limit = 1, includeAdultContents = false)
val page1 = repository.findRecentDebutCreators(now, offset = 1L, limit = 1, includeAdultContents = false)
val page2 = repository.findRecentDebutCreators(now, offset = 2L, limit = 1, includeAdultContents = false)
val page0 = repository.findRecentDebutCreators(now, offset = 0L, limit = 1, includeAdultContents = false, lang = Lang.KO)
val page1 = repository.findRecentDebutCreators(now, offset = 1L, limit = 1, includeAdultContents = false, lang = Lang.KO)
val page2 = repository.findRecentDebutCreators(now, offset = 2L, limit = 1, includeAdultContents = false, lang = Lang.KO)
val pagedCreatorIds = (page0 + page1 + page2).map { it.creatorId }
assertEquals(setOf(creator1.id, creator2.id, creator3.id), pagedCreatorIds.toSet())
@@ -1729,7 +1738,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
saveAudioContent(creator2, now.minusDays(5), isActive = true)
flushAndClear()
val creators = repository.findRecentDebutCreators(now, limit = 10)
val creators = repository.findRecentDebutCreators(now, limit = 10, lang = Lang.KO)
assertEquals(2, creators.size)
assertEquals(setOf(creator1.id, creator2.id), creators.map { it.creatorId }.toSet())
@@ -2111,7 +2120,10 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
val inactiveCreator = saveMember("cheer-detail-inactive", MemberRole.CREATOR, isActive = false)
flushAndClear()
val details = repository.findCheerCreatorRecommendationDetails(listOf(activeCreator.id!!, inactiveCreator.id!!, 999L))
val details = repository.findCheerCreatorRecommendationDetails(
listOf(activeCreator.id!!, inactiveCreator.id!!, 999L),
lang = Lang.KO
)
val detailById = details.associateBy { it.creatorId }
assertEquals(setOf(activeCreator.id), detailById.keys)
@@ -2131,7 +2143,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
val details = repository.findCheerCreatorRecommendationDetails(
listOf(viewerBlockedCreator.id!!, creatorBlockedViewer.id!!, visibleCreator.id!!),
memberId = viewer.id
memberId = viewer.id,
lang = Lang.KO
)
assertEquals(listOf(visibleCreator.id), details.map { it.creatorId })
@@ -2150,7 +2163,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
val details = repository.findCheerCreatorRecommendationDetails(
listOf(viewer.id!!, activeFollowedCreator.id!!, inactiveFollowedCreator.id!!, visibleCreator.id!!),
memberId = viewer.id
memberId = viewer.id,
lang = Lang.KO
)
assertEquals(listOf(inactiveFollowedCreator.id, visibleCreator.id), details.map { it.creatorId })
@@ -2167,7 +2181,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
val details = repository.findCheerCreatorRecommendationDetails(
listOf(viewer.id!!, activeFollowedCreator.id!!, visibleCreator.id!!),
memberId = null
memberId = null,
lang = Lang.KO
)
assertEquals(listOf(viewer.id, activeFollowedCreator.id, visibleCreator.id), details.map { it.creatorId })
@@ -2178,7 +2193,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
fun shouldReturnEmptyCheerCreatorRecommendationDetailsWhenIdsAreEmpty() {
assertEquals(
emptyList<HomeCheerCreatorRecommendationRecord>(),
repository.findCheerCreatorRecommendationDetails(emptyList())
repository.findCheerCreatorRecommendationDetails(emptyList(), lang = Lang.KO)
)
}
@@ -2216,7 +2231,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
val details = repository.findPopularCommunityRecommendationDetails(
listOf(eligible.id!!, paid.id!!, fixed.id!!, adult.id!!, inactivePost.id!!, inactiveCreatorPost.id!!, 999L),
memberId = member.id,
includeAdultCommunities = false
includeAdultCommunities = false,
lang = Lang.KO
)
val detailById = details.associateBy { it.communityId }
@@ -2244,7 +2260,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
val details = repository.findPopularCommunityRecommendationDetails(
listOf(free.id!!),
memberId = null,
includeAdultCommunities = false
includeAdultCommunities = false,
lang = Lang.KO
)
assertEquals(listOf(free.id), details.map { it.communityId })
@@ -2265,7 +2282,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
val details = repository.findPopularCommunityRecommendationDetails(
listOf(adult.id!!),
includeAdultCommunities = true
includeAdultCommunities = true,
lang = Lang.KO
)
val detailById = details.associateBy { it.communityId }
@@ -2290,7 +2308,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
val details = repository.findPopularCommunityRecommendationDetails(
listOf(viewerBlockedPost.id!!, creatorBlockedPost.id!!, visiblePost.id!!),
memberId = viewer.id,
includeAdultCommunities = false
includeAdultCommunities = false,
lang = Lang.KO
)
assertEquals(listOf(visiblePost.id), details.map { it.communityId })
@@ -2301,7 +2320,7 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
fun shouldReturnEmptyPopularCommunityRecommendationDetailsWhenIdsAreEmpty() {
assertEquals(
emptyList<HomePopularCommunityRecommendationRecord>(),
repository.findPopularCommunityRecommendationDetails(emptyList(), includeAdultCommunities = false)
repository.findPopularCommunityRecommendationDetails(emptyList(), includeAdultCommunities = false, lang = Lang.KO)
)
}
@@ -2347,7 +2366,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
includeAdultGenres = false,
genreLimit = 2,
creatorLimit = 8
creatorLimit = 8,
lang = Lang.KO
)
assertEquals(2, recommendations.size)
@@ -2377,7 +2397,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
includeAdultGenres = false,
genreLimit = 1,
creatorLimit = 8
creatorLimit = 8,
lang = Lang.KO
)
assertEquals(listOf(theme.id), recommendations.map { it.genreId })
@@ -2417,7 +2438,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
includeAdultGenres = false,
genreLimit = 1,
creatorLimit = 8
creatorLimit = 8,
lang = Lang.KO
)
assertEquals(listOf(fallbackTheme.id), recommendations.map { it.genreId })
@@ -2446,7 +2468,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
includeAdultGenres = false,
genreLimit = 1,
creatorLimit = 8
creatorLimit = 8,
lang = Lang.KO
)
val creatorIds = recommendations.single().creators.map { it.creatorId }
@@ -2477,7 +2500,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
includeAdultGenres = false,
genreLimit = 1,
creatorLimit = 8
creatorLimit = 8,
lang = Lang.KO
)
val creatorIds = recommendations.single().creators.map { it.creatorId }
@@ -2500,7 +2524,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
memberId = null,
includeAdultGenres = false,
genreLimit = 1,
creatorLimit = 8
creatorLimit = 8,
lang = Lang.KO
)
assertEquals(theme.id, recommendations.single().genreId)
@@ -2551,7 +2576,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
includeAdultGenres = false,
genreLimit = 2,
creatorLimit = 8
creatorLimit = 8,
lang = Lang.KO
)
assertEquals(2, recommendations.size)
@@ -2589,7 +2615,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
memberId = null,
includeAdultGenres = false,
genreLimit = 5,
creatorLimit = 8
creatorLimit = 8,
lang = Lang.KO
)
assertEquals(true, recommendations.size >= 5)
@@ -2633,7 +2660,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
memberId = viewer.id!!,
includeAdultGenres = false,
genreLimit = 5,
creatorLimit = 8
creatorLimit = 8,
lang = Lang.KO
)
assertEquals(true, recommendations.size >= 5)
@@ -2664,7 +2692,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
memberId = null,
includeAdultGenres = false,
genreLimit = 2,
creatorLimit = 8
creatorLimit = 8,
lang = Lang.KO
)
assertEquals(3, recommendations.size)
@@ -2695,7 +2724,8 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
memberId = null,
includeAdultGenres = false,
genreLimit = 1,
creatorLimit = 2
creatorLimit = 2,
lang = Lang.KO
)
assertEquals(1, recommendations.size)
@@ -2724,13 +2754,15 @@ class DefaultHomeRecommendationQueryRepositoryTest @Autowired constructor(
memberId = null,
includeAdultGenres = false,
genreLimit = 5,
creatorLimit = 8
creatorLimit = 8,
lang = Lang.KO
)
val visible = repository.findGenreCreatorRecommendations(
memberId = null,
includeAdultGenres = true,
genreLimit = 5,
creatorLimit = 8
creatorLimit = 8,
lang = Lang.KO
)
assertEquals(false, hidden.any { it.genreId == adultTheme.id })
@@ -1,7 +1,6 @@
package kr.co.vividnext.sodalive.v2.recommendation.application
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityDisplayContent
@@ -32,12 +31,10 @@ import java.time.ZoneOffset
class HomeRecommendationQueryServiceTest {
private val port = FakeHomeRecommendationQueryPort()
private val snapshotPort = FakeHomeRecommendationSnapshotPort()
private val langContext = LangContext()
private val creatorCommunityTranslationService = Mockito.mock(CreatorCommunityTranslationService::class.java)
private val service = HomeRecommendationQueryService(
port,
snapshotPort,
langContext = langContext,
creatorCommunityTranslationService = creatorCommunityTranslationService
)
@@ -79,7 +76,7 @@ class HomeRecommendationQueryServiceTest {
@Test
@DisplayName("홈 라이브 추천은 기본 20개를 최신순 조회 포트에 위임한다")
fun shouldFindLatestLiveRecommendationsWithDefaultLimit() {
val recommendations = service.findLiveRecommendations(memberId = 100L)
val recommendations = service.findLiveRecommendations(memberId = 100L, lang = Lang.KO)
assertEquals(20, port.liveLimit)
assertEquals(100L, port.liveMemberId)
@@ -95,7 +92,8 @@ class HomeRecommendationQueryServiceTest {
memberId = 100L,
includeAdultLives = true,
isViewerCreator = true,
effectiveViewerGender = Gender.MALE
effectiveViewerGender = Gender.MALE,
lang = Lang.KO
)
assertEquals(40, port.liveOffset)
@@ -131,7 +129,7 @@ class HomeRecommendationQueryServiceTest {
@Test
@DisplayName("최근 활동 크리에이터는 기본 10개를 최신 활동 조회 포트에 위임한다")
fun shouldFindRecentlyActiveCreatorsWithDefaultLimit() {
val creators = service.findRecentlyActiveCreators(memberId = 100L)
val creators = service.findRecentlyActiveCreators(memberId = 100L, lang = Lang.KO)
assertEquals(10, port.activeCreatorLimit)
assertEquals(100L, port.activeCreatorMemberId)
@@ -142,7 +140,12 @@ class HomeRecommendationQueryServiceTest {
@Test
@DisplayName("최근 활동 크리에이터는 성인 활동 노출 여부를 조회 포트에 위임한다")
fun shouldFindRecentlyActiveCreatorsWithAdultVisibilityPolicy() {
val creators = service.findRecentlyActiveCreators(limit = 8, memberId = 101L, includeAdultActivities = true)
val creators = service.findRecentlyActiveCreators(
limit = 8,
memberId = 101L,
includeAdultActivities = true,
lang = Lang.KO
)
assertEquals(8, port.activeCreatorLimit)
assertEquals(101L, port.activeCreatorMemberId)
@@ -155,7 +158,7 @@ class HomeRecommendationQueryServiceTest {
fun shouldFindRecentDebutCreatorsWithDefaultLimitAndNow() {
val now = LocalDateTime.of(2026, 5, 31, 10, 0)
val creators = service.findRecentDebutCreators(now, memberId = 102L)
val creators = service.findRecentDebutCreators(now, memberId = 102L, lang = Lang.KO)
assertEquals(now, port.recentDebutNow)
assertEquals(102L, port.recentDebutMemberId)
@@ -168,7 +171,7 @@ class HomeRecommendationQueryServiceTest {
fun shouldFindFirstAudioContentsWithDefaultLimitAndNow() {
val now = LocalDateTime.of(2026, 5, 31, 10, 0)
val contents = service.findFirstAudioContents(now, memberId = 103L)
val contents = service.findFirstAudioContents(now, memberId = 103L, lang = Lang.KO)
assertEquals(now, port.firstAudioNow)
assertEquals(103L, port.firstAudioMemberId)
@@ -185,7 +188,6 @@ class HomeRecommendationQueryServiceTest {
port,
snapshotPort,
fallback,
langContext = langContext,
creatorCommunityTranslationService = creatorCommunityTranslationService
)
val oldSnapshotAt = LocalDateTime.of(2026, 5, 28, 23, 59, 59)
@@ -293,7 +295,6 @@ class HomeRecommendationQueryServiceTest {
port,
snapshotPort,
fallback,
langContext = langContext,
creatorCommunityTranslationService = creatorCommunityTranslationService
)
port.aiCharacterDetails = listOf(
@@ -324,7 +325,6 @@ class HomeRecommendationQueryServiceTest {
port,
snapshotPort,
fallback,
langContext = langContext,
creatorCommunityTranslationService = creatorCommunityTranslationService
)
@@ -346,7 +346,6 @@ class HomeRecommendationQueryServiceTest {
port,
snapshotPort,
fallback,
langContext = langContext,
creatorCommunityTranslationService = creatorCommunityTranslationService
)
snapshotPort.replaceSnapshots(
@@ -375,7 +374,6 @@ class HomeRecommendationQueryServiceTest {
port,
snapshotPort,
fallback,
langContext = langContext,
creatorCommunityTranslationService = creatorCommunityTranslationService
)
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
@@ -408,7 +406,6 @@ class HomeRecommendationQueryServiceTest {
port,
snapshotPort,
fallback,
langContext = langContext,
creatorCommunityTranslationService = creatorCommunityTranslationService
)
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
@@ -435,7 +432,6 @@ class HomeRecommendationQueryServiceTest {
snapshotPort,
cheerCreatorSnapshotFallbackService = fallback,
clock = fixedClock(),
langContext = langContext,
creatorCommunityTranslationService = creatorCommunityTranslationService
)
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
@@ -459,7 +455,7 @@ class HomeRecommendationQueryServiceTest {
)
)
val creators = service.findCheerCreatorRecommendations(memberId = 104L)
val creators = service.findCheerCreatorRecommendations(memberId = 104L, lang = Lang.KO)
assertEquals(null, fallback.offset)
assertEquals(null, fallback.limit)
@@ -480,7 +476,6 @@ class HomeRecommendationQueryServiceTest {
snapshotPort,
cheerCreatorSnapshotFallbackService = fallback,
clock = fixedClock(),
langContext = langContext,
creatorCommunityTranslationService = creatorCommunityTranslationService
)
port.cheerCreatorDetails = listOf(
@@ -491,7 +486,7 @@ class HomeRecommendationQueryServiceTest {
)
)
val creators = service.findCheerCreatorRecommendations(memberId = 104L)
val creators = service.findCheerCreatorRecommendations(memberId = 104L, lang = Lang.KO)
assertEquals(0L, fallback.offset)
assertEquals(16, fallback.limit)
@@ -511,7 +506,6 @@ class HomeRecommendationQueryServiceTest {
snapshotPort,
cheerCreatorSnapshotFallbackService = fallback,
clock = fixedClock(),
langContext = langContext,
creatorCommunityTranslationService = creatorCommunityTranslationService
)
val staleSnapshotAt = LocalDateTime.of(2026, 7, 8, 14, 59, 59)
@@ -528,7 +522,7 @@ class HomeRecommendationQueryServiceTest {
)
)
val creators = service.findCheerCreatorRecommendations(memberId = 104L)
val creators = service.findCheerCreatorRecommendations(memberId = 104L, lang = Lang.KO)
assertEquals(0L, fallback.offset)
assertEquals(16, fallback.limit)
@@ -548,7 +542,6 @@ class HomeRecommendationQueryServiceTest {
snapshotPort,
cheerCreatorSnapshotFallbackService = fallback,
clock = fixedClock(),
langContext = langContext,
creatorCommunityTranslationService = creatorCommunityTranslationService
)
snapshotPort.replaceSnapshots(
@@ -557,7 +550,7 @@ class HomeRecommendationQueryServiceTest {
emptyList()
)
val creators = service.findCheerCreatorRecommendations(memberId = 104L)
val creators = service.findCheerCreatorRecommendations(memberId = 104L, lang = Lang.KO)
assertEquals(null, fallback.offset)
assertEquals(null, fallback.limit)
@@ -572,7 +565,6 @@ class HomeRecommendationQueryServiceTest {
port,
snapshotPort,
clock = fixedClock(),
langContext = langContext,
creatorCommunityTranslationService = creatorCommunityTranslationService
)
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
@@ -631,7 +623,11 @@ class HomeRecommendationQueryServiceTest {
)
)
val communities = service.findPopularCommunityRecommendations(memberId = 105L, includeAdultCommunities = true)
val communities = service.findPopularCommunityRecommendations(
memberId = 105L,
includeAdultCommunities = true,
lang = Lang.KO
)
assertEquals((1L..11L).toList(), port.popularCommunityDetailIds)
assertEquals(105L, port.popularCommunityMemberId)
@@ -652,7 +648,6 @@ class HomeRecommendationQueryServiceTest {
port,
snapshotPort,
clock = fixedClock(),
langContext = langContext,
creatorCommunityTranslationService = creatorCommunityTranslationService
)
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
@@ -681,7 +676,7 @@ class HomeRecommendationQueryServiceTest {
)
}
val communities = service.findPopularCommunityRecommendations()
val communities = service.findPopularCommunityRecommendations(lang = Lang.KO)
assertEquals(20, port.popularCommunityDetailIds.size)
assertEquals(10, communities.size)
@@ -692,7 +687,6 @@ class HomeRecommendationQueryServiceTest {
@Test
@DisplayName("인기 커뮤니티 추천은 최종 노출 게시물만 일본어 표시 본문으로 일괄 치환하고 원문 폴백을 유지한다")
fun shouldTranslateOnlyFinalPopularCommunityRecommendationsInOneJapaneseBatch() {
langContext.setLang(Lang.JA)
val snapshotAt = LocalDateTime.of(2026, 7, 9, 14, 59, 59)
snapshotPort.replaceSnapshots(
RecommendedSectionType.POPULAR_COMMUNITY,
@@ -718,11 +712,10 @@ class HomeRecommendationQueryServiceTest {
port,
snapshotPort,
clock = fixedClock(),
langContext = langContext,
creatorCommunityTranslationService = creatorCommunityTranslationService
)
val communities = translatedService.findPopularCommunityRecommendations(limit = 2)
val communities = translatedService.findPopularCommunityRecommendations(limit = 2, lang = Lang.JA)
assertEquals(listOf(1L, 3L), communities.map { it.communityId })
assertEquals(listOf("translated-1-ja", "current-original-3"), communities.map { it.content })
@@ -741,7 +734,6 @@ class HomeRecommendationQueryServiceTest {
snapshotPort,
popularCommunitySnapshotFallbackService = fallback,
clock = fixedClock(),
langContext = langContext,
creatorCommunityTranslationService = creatorCommunityTranslationService
)
val staleSnapshotAt = LocalDateTime.of(2026, 7, 8, 14, 59, 59)
@@ -768,7 +760,7 @@ class HomeRecommendationQueryServiceTest {
)
)
val communities = service.findPopularCommunityRecommendations(memberId = 105L)
val communities = service.findPopularCommunityRecommendations(memberId = 105L, lang = Lang.KO)
assertEquals(0L, fallback.offset)
assertEquals(20, fallback.limit)
@@ -788,7 +780,6 @@ class HomeRecommendationQueryServiceTest {
snapshotPort,
popularCommunitySnapshotFallbackService = fallback,
clock = fixedClock(),
langContext = langContext,
creatorCommunityTranslationService = creatorCommunityTranslationService
)
snapshotPort.replaceSnapshots(
@@ -797,7 +788,7 @@ class HomeRecommendationQueryServiceTest {
emptyList()
)
val communities = service.findPopularCommunityRecommendations(memberId = 105L)
val communities = service.findPopularCommunityRecommendations(memberId = 105L, lang = Lang.KO)
assertEquals(null, fallback.offset)
assertEquals(null, fallback.limit)
@@ -809,8 +800,11 @@ class HomeRecommendationQueryServiceTest {
@DisplayName("최신 스냅샷이 없으면 AI 캐릭터/최근 응원/인기 커뮤니티 추천은 빈 배열을 반환한다")
fun shouldReturnEmptyListWhenLatestSnapshotsDoNotExist() {
assertEquals(emptyList<HomeAiCharacterRecommendationRecord>(), service.findAiCharacterRecommendations())
assertEquals(emptyList<HomeCheerCreatorRecommendationRecord>(), service.findCheerCreatorRecommendations())
assertEquals(emptyList<HomePopularCommunityRecommendationRecord>(), service.findPopularCommunityRecommendations())
assertEquals(emptyList<HomeCheerCreatorRecommendationRecord>(), service.findCheerCreatorRecommendations(lang = Lang.KO))
assertEquals(
emptyList<HomePopularCommunityRecommendationRecord>(),
service.findPopularCommunityRecommendations(lang = Lang.KO)
)
}
@Test
@@ -858,7 +852,8 @@ class HomeRecommendationQueryServiceTest {
val recommendations = service.findGenreCreatorRecommendations(
memberId = 100L,
includeAdultGenres = true
includeAdultGenres = true,
lang = Lang.KO
)
assertEquals(100L, port.genreCreatorMemberId)
@@ -912,7 +907,8 @@ class HomeRecommendationQueryServiceTest {
memberId = 100L,
includeAdultGenres = false,
genreLimit = 2,
creatorLimit = 8
creatorLimit = 8,
lang = Lang.KO
)
assertEquals(listOf(1L, 3L), recommendations.map { it.genreId })
@@ -958,7 +954,8 @@ class HomeRecommendationQueryServiceTest {
memberId = null,
includeAdultGenres = false,
genreLimit = 2,
creatorLimit = 8
creatorLimit = 8,
lang = Lang.KO
)
assertEquals(listOf(1L, 3L), recommendations.map { it.genreId })
@@ -1009,7 +1006,8 @@ class HomeRecommendationQueryServiceTest {
memberId = null,
includeAdultGenres = false,
genreLimit = 2,
creatorLimit = 8
creatorLimit = 8,
lang = Lang.KO
)
assertEquals(listOf(1L, 3L), recommendations.map { it.genreId })
@@ -1038,7 +1036,8 @@ class HomeRecommendationQueryServiceTest {
memberId = 100L,
includeAdultGenres = false,
genreLimit = 5,
creatorLimit = 8
creatorLimit = 8,
lang = Lang.KO
)
assertEquals(listOf(1L), recommendations.map { it.genreId })
@@ -1085,7 +1084,8 @@ class HomeRecommendationQueryServiceTest {
memberId = 100L,
includeAdultGenres = false,
genreLimit = 2,
creatorLimit = 8
creatorLimit = 8,
lang = Lang.KO
)
assertEquals(listOf(1L, 2L), recommendations.map { it.genreId })
@@ -1130,6 +1130,7 @@ class HomeRecommendationQueryServiceTest {
var genreCreatorCreatorLimit: Int? = null
val liveRecommendations = listOf(
HomeLiveRecommendationRecord(
creatorMemberId = 10L,
liveRoomId = 1L,
creatorNickname = "creator",
creatorProfileImage = "profile.png",
@@ -1196,7 +1197,8 @@ class HomeRecommendationQueryServiceTest {
memberId: Long?,
includeAdultLives: Boolean,
isViewerCreator: Boolean,
effectiveViewerGender: Gender?
effectiveViewerGender: Gender?,
lang: Lang
): List<HomeLiveRecommendationRecord> {
liveOffset = offset
liveLimit = limit
@@ -1223,7 +1225,8 @@ class HomeRecommendationQueryServiceTest {
override fun findRecentlyActiveCreators(
limit: Int,
memberId: Long?,
includeAdultActivities: Boolean
includeAdultActivities: Boolean,
lang: Lang
): List<RecentlyActiveCreatorRecord> {
activeCreatorLimit = limit
activeCreatorMemberId = memberId
@@ -1236,7 +1239,8 @@ class HomeRecommendationQueryServiceTest {
offset: Long,
limit: Int,
memberId: Long?,
includeAdultContents: Boolean
includeAdultContents: Boolean,
lang: Lang
): List<RecentDebutCreatorRecord> {
recentDebutNow = now
recentDebutOffset = offset
@@ -1299,7 +1303,8 @@ class HomeRecommendationQueryServiceTest {
override fun findCheerCreatorRecommendationDetails(
creatorIds: List<Long>,
memberId: Long?
memberId: Long?,
lang: Lang
): List<HomeCheerCreatorRecommendationRecord> {
cheerCreatorDetailIds = creatorIds
cheerCreatorMemberId = memberId
@@ -1309,7 +1314,8 @@ class HomeRecommendationQueryServiceTest {
override fun findPopularCommunityRecommendationDetails(
communityIds: List<Long>,
memberId: Long?,
includeAdultCommunities: Boolean
includeAdultCommunities: Boolean,
lang: Lang
): List<HomePopularCommunityRecommendationRecord> {
popularCommunityDetailIds = communityIds
popularCommunityMemberId = memberId
@@ -1321,7 +1327,8 @@ class HomeRecommendationQueryServiceTest {
memberId: Long?,
includeAdultGenres: Boolean,
genreLimit: Int,
creatorLimit: Int
creatorLimit: Int,
lang: Lang
): List<HomeGenreCreatorRecommendationGroup> {
genreCreatorMemberId = memberId
genreCreatorIncludeAdultGenres = includeAdultGenres
@@ -0,0 +1,167 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
import kr.co.vividnext.sodalive.chat.room.ChatMessage
import kr.co.vividnext.sodalive.chat.room.ChatParticipant
import kr.co.vividnext.sodalive.chat.room.ChatRoom
import kr.co.vividnext.sodalive.chat.room.ParticipantType
import kr.co.vividnext.sodalive.chat.room.repository.ChatRoomRepository
import kr.co.vividnext.sodalive.configs.QueryDslConfig
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.following.CreatorFollowing
import kr.co.vividnext.sodalive.v2.chat.service.ChatRoomListService
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatMessageRepository
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatParticipantRepository
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatRoomRepository
import org.hibernate.SessionFactory
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource
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.data.domain.PageRequest
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 UserCreatorChatNameJoinRepositoryTest @Autowired constructor(
private val roomRepository: UserCreatorChatRoomRepository,
private val messageRepository: UserCreatorChatMessageRepository,
private val participantRepository: UserCreatorChatParticipantRepository,
private val aiRoomRepository: ChatRoomRepository,
private val entityManager: EntityManager,
private val jdbcTemplate: JdbcTemplate
) {
@ParameterizedTest
@EnumSource(Lang::class)
@DisplayName("DM 목록·히스토리·상대 프로필은 요청 Lang을 조인하고 USER·미등록·비활성 행을 유지한다")
fun shouldJoinRequestedNamesWithoutStandaloneNameQueries(lang: Lang) {
val viewer = member("viewer", MemberRole.USER)
val opponents = listOf(
member("creator", MemberRole.CREATOR),
member("inactive", MemberRole.CREATOR, false),
member("missing", MemberRole.CREATOR, null),
member("user", MemberRole.USER)
)
val rooms = opponents.map { dmRoom(viewer, it) }
entityManager.flush()
entityManager.clear()
val statistics = entityManager.entityManagerFactory.unwrap(SessionFactory::class.java).statistics
statistics.clear()
val listed = roomRepository.findDmChatListRooms(
viewer.id!!,
null,
null,
null,
false,
lang,
PageRequest.of(0, 20)
)
val expectedCreator = if (lang == Lang.KO) "creator" else "${lang.name}-creator"
assertEquals(setOf(expectedCreator, "inactive", "missing", "user"), listed.map { it.targetName }.toSet())
assertEquals(4, listed.size)
assertEquals(1L, statistics.queryExecutionCount)
assertEquals(0L, statistics.entityLoadCount)
rooms.zip(opponents).forEach { (room, opponent) ->
statistics.clear()
val expectedName = if (opponent.id == opponents.first().id) expectedCreator else opponent.nickname
val messages = messageRepository.findMessageHistory(room, null, lang, PageRequest.of(0, 20))
assertEquals(listOf(expectedName, "viewer"), messages.map { it.senderNickname })
assertEquals(listOf("opponent body", "viewer body"), messages.map { it.textMessage })
assertEquals(1L, statistics.queryExecutionCount)
assertEquals(0L, statistics.entityLoadCount)
statistics.clear()
val previous = messageRepository.findMessageHistory(room, messages.first().messageId, lang, PageRequest.of(0, 20))
assertEquals(listOf(messages.last().messageId), previous.map { it.messageId })
assertEquals(1L, statistics.queryExecutionCount)
statistics.clear()
val profile = participantRepository.findActiveOpponentProfile(room.id!!, viewer.id!!, lang)
assertEquals(expectedName, profile!!.nickname)
assertEquals(1L, statistics.queryExecutionCount)
assertEquals(0L, statistics.entityLoadCount)
}
}
@Test
@DisplayName("통합 채팅 목록은 DM만 EN 별칭을 조인하고 AI 캐릭터 이름·팔로우 필터·커서를 유지한다")
fun shouldKeepAiCharacterNameAndFollowingCursorWhenJoiningDmNames() {
val viewer = member("viewer", MemberRole.USER)
val creator = member("creator", MemberRole.CREATOR)
val room = dmRoom(viewer, creator)
val character = ChatCharacter("join-character", "AI character", "description", systemPrompt = "prompt").apply {
creatorMember = creator
}
entityManager.persist(character)
val aiRoom = ChatRoom("join-ai-session", "AI room")
entityManager.persist(aiRoom)
entityManager.persist(ChatParticipant(aiRoom, ParticipantType.USER, member = viewer))
val characterParticipant = ChatParticipant(aiRoom, ParticipantType.CHARACTER, character = character)
entityManager.persist(characterParticipant)
entityManager.persist(ChatMessage("AI body", aiRoom, characterParticipant))
entityManager.persist(
CreatorFollowing().apply {
member = viewer
this.creator = creator
}
)
entityManager.flush()
entityManager.clear()
val statistics = entityManager.entityManagerFactory.unwrap(SessionFactory::class.java).statistics
statistics.clear()
val service = ChatRoomListService(aiRoomRepository, roomRepository, "https://cdn.test")
val first = service.getRooms(viewer, Lang.EN, limit = 1, followedCreatorsOnly = true)
val second = service.getRooms(viewer, Lang.EN, cursor = first.nextCursor, limit = 1, followedCreatorsOnly = true)
assertEquals(true, first.hasMore)
assertEquals(false, second.hasMore)
assertEquals(listOf("AI character", "EN-creator"), (first.rooms + second.rooms).map { it.targetName })
assertEquals(listOf(aiRoom.id, room.id), (first.rooms + second.rooms).map { it.roomId })
assertEquals(4L, statistics.queryExecutionCount, statistics.queries.joinToString("\n"))
assertEquals(0L, statistics.entityLoadCount)
}
private fun member(nickname: String, role: MemberRole, activeAlias: Boolean? = true): Member {
val member = Member(password = "pw", nickname = nickname, role = role)
entityManager.persist(member)
if (activeAlias != null) {
listOf(Lang.JA, Lang.EN).forEach { lang ->
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.name,
"${lang.name}-$nickname",
activeAlias
)
}
}
return member
}
private fun dmRoom(viewer: Member, opponent: Member): UserCreatorChatRoom {
val room = roomRepository.save(UserCreatorChatRoom())
val mine = participantRepository.save(UserCreatorChatParticipant(room, viewer))
val theirs = participantRepository.save(UserCreatorChatParticipant(room, opponent))
val now = LocalDateTime.now()
messageRepository.save(UserCreatorChatMessage(room, mine, UserCreatorChatMessageType.TEXT, "viewer body"))
.createdAt = now.minusMinutes(2)
messageRepository.save(UserCreatorChatMessage(room, theirs, UserCreatorChatMessageType.TEXT, "opponent body"))
.createdAt = now.minusMinutes(1)
messageRepository.save(
UserCreatorChatMessage(room, theirs, UserCreatorChatMessageType.TEXT, "inactive body", isActive = false)
)
return room
}
}
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberKind
import kr.co.vividnext.sodalive.member.MemberRepository
@@ -112,7 +113,7 @@ class UserCreatorChatServiceIntegrationTest @Autowired constructor(
entityManager.clear()
val exception = assertThrows(SodaException::class.java) {
service.sendTextMessageByWebSocket(user.id!!, room.id!!, "hello")
service.sendTextMessageByWebSocket(user.id!!, room.id!!, "hello", Lang.KO)
}
assertEquals("message.error.recipient_not_found", exception.messageKey)
@@ -6,11 +6,16 @@ import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.fcm.FcmDeepLinkValue
import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.fcm.FcmEventType
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.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatMessageItemDto
import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatMessageRow
import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatOpponentProfile
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatMessageRepository
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatParticipantRepository
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatRoomRepository
@@ -70,6 +75,8 @@ class UserCreatorChatServiceTest {
applicationEventPublisher = eventPublisher,
objectMapper = ObjectMapper(),
s3Uploader = s3Uploader,
localizedNameService = Mockito.mock(CreatorLocalizedNameService::class.java),
langContext = LangContext(),
bucket = "test-bucket",
cloudFrontHost = "https://cdn.test"
)
@@ -140,13 +147,15 @@ class UserCreatorChatServiceTest {
}
val room = room(10L)
val userParticipant = participant(100L, room, user)
val creatorParticipant = participant(101L, room, creator)
Mockito.`when`(roomRepository.findByIdAndIsActiveTrue(10L)).thenReturn(room)
Mockito.`when`(participantRepository.findActiveByRoomIdAndMemberId(10L, 1L)).thenReturn(userParticipant)
Mockito.`when`(participantRepository.findActiveOpponent(10L, 1L)).thenReturn(creatorParticipant)
Mockito.`when`(participantRepository.findActiveOpponentProfile(10L, 1L, Lang.KO))
.thenReturn(UserCreatorChatOpponentProfile(creator.nickname, creator.profileImage))
Mockito.`when`(
messageRepository.findByChatRoomAndIsActiveTrueOrderByIdDesc(
messageRepository.findMessageHistory(
room,
null,
Lang.KO,
PageRequest.of(0, 20)
)
).thenReturn(emptyList())
@@ -159,7 +168,7 @@ class UserCreatorChatServiceTest {
assertEquals(emptyList<UserCreatorChatMessageItemDto>(), response.messages)
assertFalse(response.hasMore)
assertEquals(null, response.nextCursor)
Mockito.verify(participantRepository).findActiveOpponent(10L, 1L)
Mockito.verify(participantRepository).findActiveOpponentProfile(10L, 1L, Lang.KO)
}
@Test
@@ -169,13 +178,15 @@ class UserCreatorChatServiceTest {
val creator = member(2L, "creator")
val room = room(10L)
val userParticipant = participant(100L, room, user)
val creatorParticipant = participant(101L, room, creator)
Mockito.`when`(roomRepository.findByIdAndIsActiveTrue(10L)).thenReturn(room)
Mockito.`when`(participantRepository.findActiveByRoomIdAndMemberId(10L, 1L)).thenReturn(userParticipant)
Mockito.`when`(participantRepository.findActiveOpponent(10L, 1L)).thenReturn(creatorParticipant)
Mockito.`when`(participantRepository.findActiveOpponentProfile(10L, 1L, Lang.KO))
.thenReturn(UserCreatorChatOpponentProfile(creator.nickname, creator.profileImage))
Mockito.`when`(
messageRepository.findByChatRoomAndIsActiveTrueOrderByIdDesc(
messageRepository.findMessageHistory(
room,
null,
Lang.KO,
PageRequest.of(0, 20)
)
).thenReturn(emptyList())
@@ -202,7 +213,7 @@ class UserCreatorChatServiceTest {
(invocation.arguments[0] as UserCreatorChatMessage).apply { id = 203L }
}
val response = service.sendTextMessageByWebSocket(memberId = 1L, roomId = 10L, textMessage = "hello")
val response = service.sendTextMessageByWebSocket(memberId = 1L, roomId = 10L, textMessage = "hello", lang = Lang.KO)
assertEquals(203L, response.messageId)
assertEquals("hello", response.textMessage)
@@ -230,7 +241,7 @@ class UserCreatorChatServiceTest {
(invocation.arguments[0] as UserCreatorChatMessage).apply { id = 204L }
}
service.sendTextMessageByWebSocket(memberId = 1L, roomId = 10L, textMessage = "hello")
service.sendTextMessageByWebSocket(memberId = 1L, roomId = 10L, textMessage = "hello", lang = Lang.KO)
val eventCaptor = ArgumentCaptor.forClass(FcmEvent::class.java)
Mockito.verify(eventPublisher).publishEvent(eventCaptor.capture())
@@ -263,7 +274,7 @@ class UserCreatorChatServiceTest {
(invocation.arguments[0] as UserCreatorChatMessage).apply { id = 207L }
}
val response = service.sendTextMessageByWebSocket(memberId = 1L, roomId = 10L, textMessage = "hello")
val response = service.sendTextMessageByWebSocket(memberId = 1L, roomId = 10L, textMessage = "hello", lang = Lang.KO)
assertEquals(207L, response.messageId)
val eventCaptor = ArgumentCaptor.forClass(FcmEvent::class.java)
@@ -421,12 +432,13 @@ class UserCreatorChatServiceTest {
Mockito.`when`(roomRepository.findByIdAndIsActiveTrue(10L)).thenReturn(room)
Mockito.`when`(participantRepository.findActiveByRoomIdAndMemberId(10L, 1L)).thenReturn(userParticipant)
Mockito.`when`(
messageRepository.findByChatRoomAndIdLessThanAndIsActiveTrueOrderByIdDesc(
messageRepository.findMessageHistory(
room,
300L,
Lang.KO,
PageRequest.of(0, 20)
)
).thenReturn(listOf(newerMessage, olderMessage))
).thenReturn(listOf(messageRow(newerMessage), messageRow(olderMessage)))
Mockito.`when`(messageRepository.existsByChatRoomAndIsActiveTrueAndIdLessThan(room, 298L)).thenReturn(true)
val response = service.getMessages(user, roomId = 10L, cursor = 300L)
@@ -435,15 +447,30 @@ class UserCreatorChatServiceTest {
assertEquals(listOf("older", "newer"), response.messages.map { it.textMessage })
assertTrue(response.hasMore)
assertEquals(298L, response.nextCursor)
Mockito.verify(messageRepository).findByChatRoomAndIdLessThanAndIsActiveTrueOrderByIdDesc(
Mockito.verify(messageRepository).findMessageHistory(
room,
300L,
Lang.KO,
PageRequest.of(0, 20)
)
}
private fun member(id: Long, nickname: String) = Member(password = "pw", nickname = nickname).apply { this.id = id }
private fun messageRow(message: UserCreatorChatMessage): UserCreatorChatMessageRow {
val sender = message.participant.member
return UserCreatorChatMessageRow(
message.id!!,
message.messageType,
message.createdAt,
message.textMessage,
message.voiceMessage,
sender.id!!,
sender.nickname,
sender.profileImage
)
}
private fun anyMessageItem(): UserCreatorChatMessageItemDto {
return Mockito.any(UserCreatorChatMessageItemDto::class.java) ?: UserCreatorChatMessageItemDto(
messageId = 0L,
@@ -0,0 +1,367 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
import com.amazonaws.services.s3.model.ObjectMetadata
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
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.jwt.TokenProvider
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberAdapter
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.token.MemberTokenRepository
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatMessage
import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatMessageType
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatMessageRepository
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatParticipantRepository
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatRoomRepository
import kr.co.vividnext.sodalive.v2.usercreatorchat.service.UserCreatorChatService
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.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
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.boot.test.web.client.TestRestTemplate
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus
import org.springframework.http.server.ServletServerHttpRequest
import org.springframework.http.server.ServletServerHttpResponse
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.mock.web.MockMultipartFile
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.ContextConfiguration
import org.springframework.transaction.support.TransactionTemplate
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes
import org.springframework.web.socket.TextMessage
import org.springframework.web.socket.WebSocketHttpHeaders
import org.springframework.web.socket.WebSocketSession
import org.springframework.web.socket.client.standard.StandardWebSocketClient
import org.springframework.web.socket.handler.TextWebSocketHandler
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.net.URI
import java.util.UUID
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = [
"spring.cache.type=none",
"jwt.secret=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWYwMTIzNDU2Nzg5YWJjZGVmMDEyMzQ1Njc4OWFiY2RlZg=="
]
)
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
class UserCreatorChatLocalizedNameIntegrationTest @Autowired constructor(
private val tokenProvider: TokenProvider,
private val tokenRepository: MemberTokenRepository,
private val authInterceptor: UserCreatorChatWebSocketAuthInterceptor,
private val handler: UserCreatorChatWebSocketHandler,
private val service: UserCreatorChatService,
private val memberRepository: MemberRepository,
private val roomRepository: UserCreatorChatRoomRepository,
private val participantRepository: UserCreatorChatParticipantRepository,
private val messageRepository: UserCreatorChatMessageRepository,
private val objectMapper: ObjectMapper,
private val jdbcTemplate: JdbcTemplate,
private val transactionTemplate: TransactionTemplate,
private val restTemplate: TestRestTemplate,
private val langContext: LangContext
) {
@LocalServerPort
private var port: Int = 0
@MockBean
private lateinit var fcmService: FcmService
@MockBean
private lateinit var s3Uploader: S3Uploader
private lateinit var creator: Member
private lateinit var recipient: Member
private lateinit var creatorToken: String
private lateinit var recipientToken: String
private var roomId: Long = 0
private val connections = mutableListOf<ClientConnection>()
@BeforeEach
fun createCommittedFixture() {
transactionTemplate.executeWithoutResult {
val suffix = UUID.randomUUID().toString()
creator = memberRepository.saveAndFlush(
Member(
email = "creator-$suffix@localized-ws.test",
password = "pw",
nickname = "original-creator",
role = MemberRole.CREATOR
)
)
recipient = memberRepository.saveAndFlush(
Member(email = "recipient-$suffix@localized-ws.test", password = "pw", nickname = "original-user")
)
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)",
creator.id!!
)
roomId = service.createOrGetRoom(creator, recipient.id!!).roomId
}
creatorToken = token(creator)
recipientToken = token(recipient)
}
@AfterEach
fun closeConnectionsAndRemoveTokens() {
connections.forEach { it.session.close() }
tokenRepository.deleteById(creator.id!!)
tokenRepository.deleteById(recipient.id!!)
}
@ParameterizedTest
@CsvSource("ja-JP, JA", "ko-KR, KO", "en-US, EN", "fr-FR, KO")
@DisplayName("실제 JWT handshake는 Accept-Language를 Lang 세션 속성으로 저장한다")
fun shouldStoreExplicitLanguageOnAuthenticatedHandshake(header: String, expected: Lang) {
val request = MockHttpServletRequest().apply {
addHeader(HttpHeaders.AUTHORIZATION, "Bearer $recipientToken")
addHeader(HttpHeaders.ACCEPT_LANGUAGE, header)
}
val attributes = mutableMapOf<String, Any>()
val accepted = authInterceptor.beforeHandshake(
ServletServerHttpRequest(request),
ServletServerHttpResponse(MockHttpServletResponse()),
handler,
attributes
)
assertTrue(accepted)
assertEquals(recipient.id, attributes[UserCreatorChatWebSocketAuthInterceptor.MEMBER_ID_ATTRIBUTE])
assertEquals(expected, attributes["lang"])
}
@Test
@DisplayName("Accept-Language가 없는 실제 인증 handshake는 KO 세션으로 연결한다")
fun shouldDefaultHandshakeLanguageToKoreanWhenHeaderIsMissing() {
val request = MockHttpServletRequest().apply { addHeader(HttpHeaders.AUTHORIZATION, "Bearer $recipientToken") }
val attributes = mutableMapOf<String, Any>()
val accepted = authInterceptor.beforeHandshake(
ServletServerHttpRequest(request),
ServletServerHttpResponse(MockHttpServletResponse()),
handler,
attributes
)
assertTrue(accepted)
assertEquals(Lang.KO, attributes["lang"])
}
@Test
@DisplayName("localhost WS와 Redis를 거친 같은 회원의 KO JA 세션은 이름만 다르고 원본 본문과 ID를 공유한다")
fun shouldDeliverDifferentNamesToSimultaneousSessionsOfTheSameRecipient() {
val korean = connect(recipientToken, "ko-KR")
val japanese = connect(recipientToken, "ja-JP")
val sender = connect(creatorToken, "ja-JP")
join(korean)
join(japanese)
join(sender)
sendText(sender, "original-creator 본문", "send-multilang")
val koreanMessage = receive(korean, "MESSAGE")["payload"]
val japaneseMessage = receive(japanese, "MESSAGE")["payload"]
val ack = receive(sender, "SEND_ACK")
assertAll(
{ assertEquals("original-creator", koreanMessage["senderNickname"].asText()) },
{ assertEquals("日本のクリエイター", japaneseMessage["senderNickname"].asText()) },
{ assertEquals("original-creator 본문", koreanMessage["textMessage"].asText()) },
{ assertEquals("original-creator 본문", japaneseMessage["textMessage"].asText()) },
{ assertEquals(koreanMessage["messageId"], japaneseMessage["messageId"]) },
{ assertEquals(creator.id!!, japaneseMessage["senderId"].asLong()) },
{ assertEquals(false, japaneseMessage["mine"].asBoolean()) },
{ assertEquals("send-multilang", ack["requestId"].asText()) },
{ assertEquals("original-creator", memberRepository.findById(creator.id!!).orElseThrow().nickname) }
)
}
@Test
@DisplayName("JA 발신자 SEND_ACK는 KO 수신자 전달과 독립적으로 현지화하고 mine과 requestId를 유지한다")
fun shouldRenderSenderAckInSenderSessionLanguageIndependentlyOfRecipient() {
val receiver = connect(recipientToken, "ko-KR")
val sender = connect(creatorToken, "ja-JP")
join(receiver)
join(sender)
sendText(sender, "ACK 본문", "sender-ja")
val received = receive(receiver, "MESSAGE")["payload"]
val ack = receive(sender, "SEND_ACK")
assertAll(
{ assertEquals("日本のクリエイター", ack["payload"]["senderNickname"].asText()) },
{ assertEquals("original-creator", received["senderNickname"].asText()) },
{ assertEquals(true, ack["payload"]["mine"].asBoolean()) },
{ assertEquals(false, received["mine"].asBoolean()) },
{ assertEquals(received["messageId"], ack["payload"]["messageId"]) },
{ assertEquals("sender-ja", ack["requestId"].asText()) }
)
}
@Test
@DisplayName("HTTP open과 메시지 페이지는 현재 요청 언어를 사용하고 이전 JA 응답을 후속 KO 조회가 바꾸지 않는다")
fun shouldRenderHttpRoomAndMessageNamesAtEachRequestBoundary() {
transactionTemplate.executeWithoutResult {
val room = roomRepository.findById(roomId).orElseThrow()
val participant = participantRepository.findActiveByRoomIdAndMemberId(roomId, creator.id!!)!!
messageRepository.saveAndFlush(
UserCreatorChatMessage(
chatRoom = room,
participant = participant,
messageType = UserCreatorChatMessageType.TEXT,
textMessage = "original-creator text"
)
)
}
val japanese = getRoomResource("open", "ja-JP")
val korean = getRoomResource("open", "ko-KR")
val messages = getRoomResource("messages", "ja-JP")
assertAll(
{ assertEquals("日本のクリエイター", japanese["opponentNickname"].asText()) },
{ assertEquals("日本のクリエイター", japanese["messages"][0]["senderNickname"].asText()) },
{ assertEquals("original-creator", korean["opponentNickname"].asText()) },
{ assertEquals("original-creator", korean["messages"][0]["senderNickname"].asText()) },
{ assertEquals("日本のクリエイター", messages["messages"][0]["senderNickname"].asText()) },
{ assertEquals("original-creator text", japanese["messages"][0]["textMessage"].asText()) }
)
}
@Test
@DisplayName("음성 REST 응답의 JA 이름과 KO WebSocket 전달은 같은 저장 메시지를 독립 렌더링한다")
fun shouldLocalizeVoiceHttpResponseWithoutLocalizingNeutralPublishedPayload() {
val receiver = connect(recipientToken, "ko-KR")
join(receiver)
Mockito.`when`(
s3Uploader.upload(
Mockito.any(InputStream::class.java) ?: ByteArrayInputStream(byteArrayOf()),
Mockito.anyString(),
Mockito.anyString(),
Mockito.any(ObjectMetadata::class.java)
)
).thenReturn("voice/localized.aac")
RequestContextHolder.setRequestAttributes(ServletRequestAttributes(MockHttpServletRequest()))
try {
langContext.setLang(Lang.JA)
val response = service.sendVoiceMessage(
creator,
roomId,
MockMultipartFile("voiceMessageFile", "voice.aac", "audio/aac", byteArrayOf(1, 2)),
"{}"
)
val delivered = receive(receiver, "MESSAGE")["payload"]
assertEquals("日本のクリエイター", response.message.senderNickname)
assertEquals("original-creator", delivered["senderNickname"].asText())
assertEquals(response.message.messageId, delivered["messageId"].asLong())
assertEquals(response.message.voiceMessageUrl, delivered["voiceMessageUrl"].asText())
assertTrue(response.deliveredRealtime)
assertEquals(false, response.pushSent)
} finally {
RequestContextHolder.resetRequestAttributes()
}
}
private fun token(member: Member): String {
val principal = MemberAdapter(member)
return tokenProvider.createToken(
UsernamePasswordAuthenticationToken(principal, "pw", principal.authorities),
member.id!!
)
}
private fun connect(token: String, language: String): ClientConnection {
val messages = LinkedBlockingQueue<JsonNode>()
val clientHandler = object : TextWebSocketHandler() {
override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
messages.add(objectMapper.readTree(message.payload))
}
}
val headers = WebSocketHttpHeaders().apply {
setBearerAuth(token)
set(HttpHeaders.ACCEPT_LANGUAGE, language)
}
val session = StandardWebSocketClient().doHandshake(
clientHandler,
headers,
URI("ws://localhost:$port${UserCreatorChatWebSocketConfig.ENDPOINT}")
).get(5, TimeUnit.SECONDS)
return ClientConnection(session, messages).also { connections.add(it) }
}
private fun join(connection: ClientConnection) {
connection.session.sendMessage(TextMessage("""{"type":"JOIN_ROOM","roomId":$roomId,"payload":{}}"""))
receive(connection, "JOINED")
}
private fun sendText(connection: ClientConnection, text: String, requestId: String) {
connection.session.sendMessage(
TextMessage(
objectMapper.writeValueAsString(
mapOf(
"type" to "SEND_TEXT",
"requestId" to requestId,
"roomId" to roomId,
"payload" to mapOf("textMessage" to text)
)
)
)
)
}
private fun receive(connection: ClientConnection, expectedType: String): JsonNode {
val envelope = connection.messages.poll(5, TimeUnit.SECONDS)
assertNotNull(envelope, "Expected $expectedType from localhost WebSocket")
assertEquals(expectedType, envelope!!["type"].asText(), envelope.toString())
return envelope
}
private fun getRoomResource(resource: String, language: String): JsonNode {
val headers = HttpHeaders().apply {
setBearerAuth(recipientToken)
set(HttpHeaders.ACCEPT_LANGUAGE, language)
}
val response = restTemplate.exchange(
"http://localhost:$port/api/v2/user-creator-chat/rooms/$roomId/$resource",
HttpMethod.GET,
HttpEntity<Void>(headers),
String::class.java
)
assertEquals(HttpStatus.OK, response.statusCode, response.body)
val body = objectMapper.readTree(response.body)
assertTrue(body["success"].asBoolean(), response.body)
return body["data"]
}
private data class ClientConnection(val session: WebSocketSession, val messages: LinkedBlockingQueue<JsonNode>)
}
@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
import kr.co.vividnext.sodalive.support.EmbeddedRedisTestConfiguration
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
@@ -14,6 +15,7 @@ import org.mockito.ArgumentCaptor
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.redis.core.StringRedisTemplate
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.TestPropertySource
@@ -35,6 +37,9 @@ import java.util.concurrent.TimeUnit
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
@TestPropertySource(properties = ["user-creator-chat.websocket.server-id=redis-test-server"])
class UserCreatorChatRedisIntegrationTest {
@MockBean
private lateinit var localizedNameService: CreatorLocalizedNameService
@Autowired
private lateinit var stringRedisTemplate: StringRedisTemplate
@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
@@ -25,7 +26,8 @@ class UserCreatorChatRoomMessageBrokerTest {
stringRedisTemplate = stringRedisTemplate,
sessionRegistry = registry,
objectMapper = objectMapper,
listenerContainer = listenerContainer
listenerContainer = listenerContainer,
localizedNameService = Mockito.mock(CreatorLocalizedNameService::class.java)
)
@Test
@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.usercreatorchat.websocket
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.v2.usercreatorchat.dto.UserCreatorChatMessageItemDto
import kr.co.vividnext.sodalive.v2.usercreatorchat.service.UserCreatorChatService
import org.junit.jupiter.api.Assertions.assertEquals
@@ -66,7 +67,7 @@ class UserCreatorChatWebSocketHandlerTest {
@DisplayName("SEND_TEXT는 메시지 저장 후 sender에게 SEND_ACK를 응답한다")
fun shouldSendAckToSenderWhenTextMessageIsSaved() {
val session = session("session-1", memberId = 1L)
Mockito.`when`(service.sendTextMessageByWebSocket(memberId = 1L, roomId = 10L, textMessage = "hello"))
Mockito.`when`(service.sendTextMessageByWebSocket(1L, 10L, "hello", Lang.KO))
.thenReturn(messageItem())
handler.handleMessage(session, textMessage("JOIN_ROOM", "join-1", 10L, "{}"))
Mockito.clearInvocations(session)
@@ -103,7 +104,8 @@ class UserCreatorChatWebSocketHandlerTest {
Mockito.verify(service, Mockito.never()).sendTextMessageByWebSocket(
Mockito.anyLong(),
Mockito.anyLong(),
Mockito.anyString()
Mockito.anyString(),
Mockito.eq(Lang.KO) ?: Lang.KO
)
}