Test #464
+1
-5
@@ -97,10 +97,7 @@ class HomeRecommendationFacade(
|
||||
lang = lang
|
||||
)
|
||||
.map { it.toItem() },
|
||||
aiCharacters = queryService.findAiCharacterRecommendations(
|
||||
limit = HOME_AI_CHARACTER_LIMIT,
|
||||
backfillRandom = true
|
||||
).map { it.toItem() },
|
||||
aiCharacters = emptyList(),
|
||||
genreCreators = queryService.findGenreCreatorRecommendations(
|
||||
memberId = member?.id,
|
||||
includeAdultGenres = includeAdult,
|
||||
@@ -357,7 +354,6 @@ class HomeRecommendationFacade(
|
||||
private const val HOME_ACTIVE_CREATOR_LIMIT = 10
|
||||
private const val HOME_RECENT_DEBUT_CREATOR_LIMIT = 10
|
||||
private const val HOME_FIRST_AUDIO_CONTENT_LIMIT = 10
|
||||
private const val HOME_AI_CHARACTER_LIMIT = 20
|
||||
private const val HOME_GENRE_CREATOR_GENRE_LIMIT = 5
|
||||
private const val HOME_GENRE_CREATOR_CREATOR_LIMIT = 8
|
||||
private const val HOME_CHEER_CREATOR_LIMIT = 8
|
||||
|
||||
+35
-8
@@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.member.MemberAdapter
|
||||
import kr.co.vividnext.sodalive.member.MemberKind
|
||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.auth.Auth
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
|
||||
import kr.co.vividnext.sodalive.member.following.CreatorFollowingRepository
|
||||
@@ -29,6 +30,8 @@ import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
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.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||
@@ -350,9 +353,29 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
||||
.andExpect(jsonPath("$.data.lives").isArray)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("메인 홈 통합 조회는 AI 캐릭터를 최신 스냅샷 기준 최대 20개까지 반환한다")
|
||||
fun shouldReturnTwentyAiCharactersOnHomeRecommendations() {
|
||||
@ParameterizedTest
|
||||
@CsvSource(
|
||||
"ANONYMOUS, ko",
|
||||
"ANONYMOUS, ja",
|
||||
"UNVERIFIED, ko",
|
||||
"UNVERIFIED, ja",
|
||||
"VERIFIED, ko",
|
||||
"VERIFIED, ja",
|
||||
"ADMIN, ko",
|
||||
"ADMIN, ja"
|
||||
)
|
||||
@DisplayName("메인 홈 통합 조회는 AI 스냅샷이 있어도 모든 조회자와 KO·JA 요청에 빈 AI 배열을 반환한다")
|
||||
fun shouldReturnEmptyAiCharactersOnHomeRecommendations(viewerType: String, language: String) {
|
||||
val member = when (viewerType) {
|
||||
"ANONYMOUS" -> null
|
||||
"ADMIN" -> saveMember("home-ai-admin", MemberRole.ADMIN)
|
||||
else -> saveMember("home-ai-viewer", MemberRole.USER)
|
||||
}
|
||||
if (viewerType == "VERIFIED") {
|
||||
entityManager.persist(
|
||||
Auth("verified", "19900101", "home-ai-ci", "home-ai-di", 1).apply { this.member = member }
|
||||
)
|
||||
}
|
||||
(1..22).forEach { index ->
|
||||
val character = saveAiCharacter("home-ai-character-$index")
|
||||
saveRecommendationSnapshot(character.id!!)
|
||||
@@ -360,10 +383,14 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
mockMvc.perform(get("/api/v2/home/recommendations"))
|
||||
val request = get("/api/v2/home/recommendations").header("Accept-Language", language)
|
||||
member?.let { request.with(user(MemberAdapter(it))) }
|
||||
|
||||
mockMvc.perform(request)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.aiCharacters.length()").value(20))
|
||||
.andExpect(jsonPath("$.data.aiCharacters").isArray)
|
||||
.andExpect(jsonPath("$.data.aiCharacters").isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -630,7 +657,7 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("AI 캐릭터 추천은 홈 통합과 전체보기 응답에 characterId와 creatorId를 함께 노출한다")
|
||||
@DisplayName("홈 통합 AI 목록은 비어 있고 전용 전체보기는 characterId와 creatorId를 함께 노출한다")
|
||||
fun shouldExposeCreatorIdOnAiCharacterRecommendations() {
|
||||
val member = saveMember("ai-character-page-viewer", MemberRole.USER)
|
||||
val character = saveAiCharacter("ai-character-api")
|
||||
@@ -640,8 +667,8 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
||||
|
||||
mockMvc.perform(get("/api/v2/home/recommendations"))
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.aiCharacters[0].characterId").value(character.id))
|
||||
.andExpect(jsonPath("$.data.aiCharacters[0].creatorId").value(character.creatorMember!!.id))
|
||||
.andExpect(jsonPath("$.data.aiCharacters").isArray)
|
||||
.andExpect(jsonPath("$.data.aiCharacters").isEmpty)
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/home/recommendations/ai-characters")
|
||||
|
||||
+70
-8
@@ -7,6 +7,7 @@ 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.CreatorCommunityTranslationService
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.application.AiCharacterSnapshotFallbackPort
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryService
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeAiCharacterRecommendationRecord
|
||||
@@ -22,6 +23,8 @@ import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecentlyActiveCreator
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotPort
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotRecord
|
||||
import kr.co.vividnext.sodalive.v2.subbanner.application.SubBannerService
|
||||
import org.junit.jupiter.api.Assertions.assertAll
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
@@ -31,10 +34,13 @@ import java.time.LocalDateTime
|
||||
|
||||
class HomeRecommendationFacadeTest {
|
||||
private val queryPort = FakeHomeRecommendationQueryPort()
|
||||
private val snapshotPort = EmptyRecommendationSnapshotPort()
|
||||
private val aiFallbackPort = FakeAiCharacterSnapshotFallbackPort()
|
||||
private val creatorCommunityTranslationService = Mockito.mock(CreatorCommunityTranslationService::class.java)
|
||||
private val queryService = HomeRecommendationQueryService(
|
||||
queryPort,
|
||||
EmptyRecommendationSnapshotPort(),
|
||||
snapshotPort,
|
||||
aiCharacterSnapshotFallbackService = aiFallbackPort,
|
||||
creatorCommunityTranslationService = creatorCommunityTranslationService
|
||||
)
|
||||
private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
|
||||
@@ -45,6 +51,22 @@ class HomeRecommendationFacadeTest {
|
||||
"https://cdn.test"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("홈 추천은 AI 스냅샷과 상세 및 무작위 보충이나 fallback 조회 없이 빈 AI 목록을 반환한다")
|
||||
fun shouldReturnEmptyAiCharactersWithoutQueryingAiRecommendations() {
|
||||
val response = facade.getHomeRecommendations(null, Lang.KO)
|
||||
|
||||
assertAll(
|
||||
{ assertTrue(response.aiCharacters.isEmpty()) },
|
||||
{ assertEquals(0, snapshotPort.aiSnapshotCalls) },
|
||||
{ assertEquals(0, snapshotPort.aiSnapshotExistenceCalls) },
|
||||
{ assertEquals(0, queryPort.aiSnapshotCalls) },
|
||||
{ assertEquals(0, queryPort.aiDetailCalls) },
|
||||
{ assertEquals(0, queryPort.aiRandomCalls) },
|
||||
{ assertEquals(0, aiFallbackPort.calls) }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("홈 첫 오디오 응답은 무료 콘텐츠의 포인트 가능 여부를 false로 보정한다")
|
||||
fun shouldNormalizePointAvailabilityForFreeFirstAudioContent() {
|
||||
@@ -150,6 +172,9 @@ class HomeRecommendationFacadeTest {
|
||||
var bannerLang: Lang? = null
|
||||
var liveIsViewerCreator: Boolean? = null
|
||||
var liveEffectiveViewerGender: Gender? = null
|
||||
var aiSnapshotCalls: Int = 0
|
||||
var aiDetailCalls: Int = 0
|
||||
var aiRandomCalls: Int = 0
|
||||
|
||||
override fun findLiveRecommendations(
|
||||
offset: Long,
|
||||
@@ -205,7 +230,10 @@ class HomeRecommendationFacadeTest {
|
||||
windowStart: LocalDateTime,
|
||||
windowEndExclusive: LocalDateTime,
|
||||
limit: Int
|
||||
): List<RecommendationSnapshotRecord> = emptyList()
|
||||
): List<RecommendationSnapshotRecord> {
|
||||
aiSnapshotCalls++
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun findCheerCreatorSnapshots(
|
||||
windowStart: LocalDateTime,
|
||||
@@ -221,9 +249,15 @@ class HomeRecommendationFacadeTest {
|
||||
|
||||
override fun findAiCharacterRecommendationDetails(
|
||||
characterIds: List<Long>
|
||||
): List<HomeAiCharacterRecommendationRecord> = emptyList()
|
||||
): List<HomeAiCharacterRecommendationRecord> {
|
||||
aiDetailCalls++
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun findRandomAiCharacterRecommendationIds(excludeCharacterIds: List<Long>, limit: Int): List<Long> = emptyList()
|
||||
override fun findRandomAiCharacterRecommendationIds(excludeCharacterIds: List<Long>, limit: Int): List<Long> {
|
||||
aiRandomCalls++
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun findCheerCreatorRecommendationDetails(
|
||||
creatorIds: List<Long>,
|
||||
@@ -248,22 +282,37 @@ class HomeRecommendationFacadeTest {
|
||||
}
|
||||
|
||||
private class EmptyRecommendationSnapshotPort : RecommendationSnapshotPort {
|
||||
var aiSnapshotCalls: Int = 0
|
||||
var aiSnapshotExistenceCalls: Int = 0
|
||||
|
||||
override fun findLatestSnapshots(
|
||||
sectionType: RecommendedSectionType,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<RecommendationSnapshotRecord> = emptyList()
|
||||
): List<RecommendationSnapshotRecord> {
|
||||
if (sectionType == RecommendedSectionType.AI_CHARACTER) aiSnapshotCalls++
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun findSnapshots(
|
||||
sectionType: RecommendedSectionType,
|
||||
snapshotAt: LocalDateTime,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<RecommendationSnapshotRecord> = emptyList()
|
||||
): List<RecommendationSnapshotRecord> {
|
||||
if (sectionType == RecommendedSectionType.AI_CHARACTER) aiSnapshotCalls++
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun existsLatestSnapshot(sectionType: RecommendedSectionType): Boolean = true
|
||||
override fun existsLatestSnapshot(sectionType: RecommendedSectionType): Boolean {
|
||||
if (sectionType == RecommendedSectionType.AI_CHARACTER) aiSnapshotExistenceCalls++
|
||||
return sectionType != RecommendedSectionType.AI_CHARACTER
|
||||
}
|
||||
|
||||
override fun existsSnapshot(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime): Boolean = true
|
||||
override fun existsSnapshot(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime): Boolean {
|
||||
if (sectionType == RecommendedSectionType.AI_CHARACTER) aiSnapshotExistenceCalls++
|
||||
return true
|
||||
}
|
||||
|
||||
override fun replaceSnapshots(
|
||||
sectionType: RecommendedSectionType,
|
||||
@@ -271,4 +320,17 @@ class HomeRecommendationFacadeTest {
|
||||
newSnapshots: List<RecommendationSnapshotRecord>
|
||||
) = Unit
|
||||
}
|
||||
|
||||
private class FakeAiCharacterSnapshotFallbackPort : AiCharacterSnapshotFallbackPort {
|
||||
var calls: Int = 0
|
||||
|
||||
override fun refreshIfMissing(
|
||||
offset: Long,
|
||||
limit: Int,
|
||||
nowUtc: LocalDateTime
|
||||
): List<RecommendationSnapshotRecord> {
|
||||
calls++
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user