test #449

Merged
klaus merged 29 commits from test into main 2026-09-10 07:58:32 +00:00
3 changed files with 143 additions and 4 deletions
Showing only changes of commit 0d44258a61 - Show all commits
@@ -1,9 +1,12 @@
package kr.co.vividnext.sodalive.v2.home.following.application
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
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService
import kr.co.vividnext.sodalive.v2.home.following.domain.FollowingNewsType
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowing
import kr.co.vividnext.sodalive.v2.home.following.port.out.HomeFollowingQueryPort
import org.springframework.stereotype.Service
@@ -16,6 +19,8 @@ import java.time.LocalDateTime
class HomeFollowingQueryService(
private val queryPort: HomeFollowingQueryPort,
private val memberContentPreferenceService: MemberContentPreferenceService,
private val langContext: LangContext,
private val creatorCommunityTranslationService: CreatorCommunityTranslationService,
private val nowProvider: () -> LocalDateTime = { LocalDateTime.now(Clock.systemUTC()) }
) {
fun findHomeFollowing(member: Member): HomeFollowing {
@@ -24,6 +29,15 @@ class HomeFollowingQueryService(
val effectiveViewerGender = member.effectiveGender()
val isViewerCreator = member.role == MemberRole.CREATOR
val now = nowProvider()
val recentNews = queryPort.findRecentNews(memberId, canViewAdultContent, now, RECENT_NEWS_LIMIT)
val communityPostIds = recentNews.mapNotNull { news ->
if (news.type == FollowingNewsType.COMMUNITY_POST) news.communityPost?.postId else null
}
val displayContents = if (communityPostIds.isEmpty()) {
emptyMap()
} else {
creatorCommunityTranslationService.findDisplayContents(communityPostIds, langContext.lang.code)
}
return HomeFollowing(
followingCreators = queryPort.findFollowingCreators(memberId, FOLLOWING_CREATORS_LIMIT),
@@ -43,7 +57,14 @@ class HomeFollowingQueryService(
now,
MONTHLY_SCHEDULES_LIMIT
),
recentNews = queryPort.findRecentNews(memberId, canViewAdultContent, now, RECENT_NEWS_LIMIT)
recentNews = recentNews.map { news ->
if (news.type != FollowingNewsType.COMMUNITY_POST) return@map news
val communityPost = news.communityPost ?: return@map news
displayContents[communityPost.postId]
?.let { displayContent -> news.copy(communityPost = communityPost.copy(content = displayContent.content)) }
?: news
}
)
}
@@ -18,6 +18,8 @@ import kr.co.vividnext.sodalive.member.MemberKind
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityDisplayContent
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService
import kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.HomeFollowingNewsInbox
import kr.co.vividnext.sodalive.v2.home.following.domain.FollowingNewsType
import kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.CreatorRankingSnapshot
@@ -63,6 +65,9 @@ class HomeFollowingEndToEndTest @Autowired constructor(
@MockBean
private lateinit var countryContext: CountryContext
@MockBean
private lateinit var creatorCommunityTranslationService: CreatorCommunityTranslationService
@Test
@DisplayName("팔로잉 탭 API는 비회원에게 200 OK와 로그인 필요 빈 섹션 응답을 반환한다")
fun shouldReturnLoginRequiredForAnonymous() {
@@ -137,6 +142,56 @@ class HomeFollowingEndToEndTest @Autowired constructor(
.andExpect(jsonPath("$.data.recentNews[?(@.communityPost.postId == ${fixture.paidPostId})]").isEmpty)
}
@Test
@DisplayName("인증 회원 팔로잉 탭은 노출된 커뮤니티 소식만 영어 표시 본문으로 반환하고 다른 소식은 유지한다")
fun shouldReturnEnglishCommunityTranslationForAuthenticatedFollowingTab() {
Mockito.doReturn("US").`when`(countryContext).countryCode
val fixture = createFixture()
val fallbackPostId = transactionTemplate.execute {
val creator = entityManager.find(Member::class.java, fixture.creatorId)
val fallbackPost = saveCommunityPost(
creator,
"historical following fallback body",
price = 0,
imagePath = "community/fallback.png"
)
saveNews(
memberId = fixture.viewer.id!!,
creatorId = fixture.creatorId,
sourceKey = "fallback-community-news",
visibleFromAtUtc = LocalDateTime.now(ZoneOffset.UTC).minusMinutes(10),
newsType = FollowingNewsType.COMMUNITY_POST,
targetId = fallbackPost.id!!
)
entityManager.flush()
fallbackPost.id!!
}!!
Mockito.`when`(creatorCommunityTranslationService.findDisplayContents(listOf(fallbackPostId, fixture.freePostId), "en"))
.thenReturn(
mapOf(
fallbackPostId to CreatorCommunityDisplayContent("current following fallback body en", false),
fixture.freePostId to CreatorCommunityDisplayContent("translated following body en", true)
)
)
mockMvc.perform(
get("/api/v2/home/following")
.header("Accept-Language", "en")
.with(user(MemberAdapter(fixture.viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.data.recentNews[0].type").value("COMMUNITY_POST"))
.andExpect(jsonPath("$.data.recentNews[0].communityPost.content").value("current following fallback body en"))
.andExpect(jsonPath("$.data.recentNews[1].type").value("CREATOR_RANKING"))
.andExpect(jsonPath("$.data.recentNews[2].type").value("COMMUNITY_POST"))
.andExpect(jsonPath("$.data.recentNews[2].communityPost.content").value("translated following body en"))
.andExpect(jsonPath("$.data.recentNews[3].type").value("AUDIO_CONTENT"))
.andExpect(jsonPath("$.data.recentNews[3].audioContent.title").value("home-following-audio"))
Mockito.verify(creatorCommunityTranslationService).findDisplayContents(listOf(fallbackPostId, fixture.freePostId), "en")
Mockito.verifyNoMoreInteractions(creatorCommunityTranslationService)
}
private fun createFixture(): Fixture {
return transactionTemplate.execute {
val now = LocalDateTime.now(ZoneOffset.UTC)
@@ -1,10 +1,16 @@
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
import kr.co.vividnext.sodalive.member.auth.Auth
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityDisplayContent
import kr.co.vividnext.sodalive.v2.creator.channel.community.translation.application.CreatorCommunityTranslationService
import kr.co.vividnext.sodalive.v2.home.following.domain.FollowingNewsType
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCommunityPostNews
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCreator
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingLive
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingNews
@@ -23,10 +29,14 @@ 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
memberContentPreferenceService,
langContext,
creatorCommunityTranslationService
) { now }
@Test
@@ -86,7 +96,12 @@ class HomeFollowingQueryServiceTest {
try {
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"))
val defaultQueryPort = RecordingHomeFollowingQueryPort()
val defaultService = HomeFollowingQueryService(defaultQueryPort, memberContentPreferenceService)
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)
@@ -103,10 +118,57 @@ class HomeFollowingQueryServiceTest {
}
}
@Test
@DisplayName("팔로잉 최근 소식은 커뮤니티 게시물만 영어 표시 본문으로 일괄 치환하고 다른 소식과 원문 폴백을 유지한다")
fun shouldTranslateOnlyCommunityPostNewsInOneEnglishBatch() {
langContext.setLang(Lang.EN)
val translatedNews = communityNews("community-1", 101L, "original-1")
val audioNews = HomeFollowingNews(
newsId = "audio-1",
type = FollowingNewsType.AUDIO_CONTENT,
visibleFromAtUtc = "2026-06-25T11:59:00Z"
)
val fallbackNews = communityNews("community-2", 102L, "historical-original-2")
queryPort.recentNews = listOf(translatedNews, audioNews, fallbackNews)
Mockito.`when`(creatorCommunityTranslationService.findDisplayContents(listOf(101L, 102L), "en"))
.thenReturn(
mapOf(
101L to CreatorCommunityDisplayContent("translated-1-en", true),
102L to CreatorCommunityDisplayContent("current-original-2", false)
)
)
val home = service.findHomeFollowing(member(14L))
assertEquals("translated-1-en", home.recentNews[0].communityPost?.content)
assertEquals(audioNews, home.recentNews[1])
assertEquals("current-original-2", home.recentNews[2].communityPost?.content)
Mockito.verify(creatorCommunityTranslationService).findDisplayContents(listOf(101L, 102L), "en")
Mockito.verifyNoMoreInteractions(creatorCommunityTranslationService)
}
private fun member(id: Long): Member {
return Member(email = "member-$id@test.com", password = "password", nickname = "member-$id").apply { this.id = id }
}
private fun communityNews(newsId: String, postId: Long, content: String): HomeFollowingNews {
return HomeFollowingNews(
newsId = newsId,
type = FollowingNewsType.COMMUNITY_POST,
visibleFromAtUtc = "2026-06-25T12:00:00Z",
communityPost = HomeFollowingCommunityPostNews(
postId = postId,
creatorProfileImage = "profile.png",
creatorNickname = "creator",
imageUrl = "image.png",
content = content,
createdAt = "2026-06-25T12:00:00Z",
likeCount = 1,
commentCount = 2
)
)
}
private class RecordingHomeFollowingQueryPort : HomeFollowingQueryPort {
var memberId: Long? = null
var followingCreatorsLimit: Int? = null
@@ -122,6 +184,7 @@ class HomeFollowingQueryServiceTest {
var recentNewsLimit: Int? = null
var recentNewsNow: LocalDateTime? = null
var recentNewsCanViewAdultContent: Boolean? = null
var recentNews: List<HomeFollowingNews> = emptyList()
override fun findFollowingCreators(memberId: Long, limit: Int): List<HomeFollowingCreator> {
this.memberId = memberId
@@ -168,7 +231,7 @@ class HomeFollowingQueryServiceTest {
recentNewsCanViewAdultContent = canViewAdultContent
recentNewsNow = nowUtc
recentNewsLimit = limit
return emptyList()
return recentNews
}
}
}