feat(home): 홈 추천 조회 로그와 회원 컨텍스트를 전달한다
This commit is contained in:
@@ -25,6 +25,7 @@ import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeLiveRecommendationReco
|
|||||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomePopularCommunityRecommendationRecord
|
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomePopularCommunityRecommendationRecord
|
||||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecentDebutCreatorRecord
|
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecentDebutCreatorRecord
|
||||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecentlyActiveCreatorRecord
|
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecentlyActiveCreatorRecord
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@@ -36,78 +37,176 @@ class HomeRecommendationFacade(
|
|||||||
@Value("\${cloud.aws.cloud-front.host}")
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
private val cloudFrontHost: String
|
private val cloudFrontHost: String
|
||||||
) {
|
) {
|
||||||
fun getHomeRecommendations(member: Member?): HomeRecommendationResponse {
|
private val log = LoggerFactory.getLogger(javaClass)
|
||||||
val now = LocalDateTime.now()
|
|
||||||
val includeAdult = resolveAdultVisibility(member)
|
|
||||||
|
|
||||||
return HomeRecommendationResponse(
|
fun getHomeRecommendations(member: Member?): HomeRecommendationResponse {
|
||||||
lives = queryService.findLiveRecommendations(
|
val startedAt = System.currentTimeMillis()
|
||||||
limit = HOME_LIVE_LIMIT,
|
return runCatching {
|
||||||
includeAdultLives = includeAdult
|
val now = LocalDateTime.now()
|
||||||
).map { it.toItem() },
|
val includeAdult = resolveAdultVisibility(member)
|
||||||
banners = queryService.findHomeBanners(HOME_BANNER_LIMIT).map { it.toItem() },
|
|
||||||
recentlyActiveCreators = queryService.findRecentlyActiveCreators(HOME_ACTIVE_CREATOR_LIMIT, includeAdult)
|
HomeRecommendationResponse(
|
||||||
.map { it.toItem() },
|
lives = queryService.findLiveRecommendations(
|
||||||
recentDebutCreators = queryService.findRecentDebutCreators(
|
limit = HOME_LIVE_LIMIT,
|
||||||
now,
|
memberId = member?.id,
|
||||||
limit = HOME_RECENT_DEBUT_CREATOR_LIMIT,
|
includeAdultLives = includeAdult
|
||||||
includeAdultContents = includeAdult
|
).map { it.toItem() },
|
||||||
|
banners = queryService.findHomeBanners(HOME_BANNER_LIMIT, member?.id).map { it.toItem() },
|
||||||
|
recentlyActiveCreators = queryService.findRecentlyActiveCreators(
|
||||||
|
HOME_ACTIVE_CREATOR_LIMIT,
|
||||||
|
member?.id,
|
||||||
|
includeAdult
|
||||||
|
)
|
||||||
|
.map { it.toItem() },
|
||||||
|
recentDebutCreators = queryService.findRecentDebutCreators(
|
||||||
|
now,
|
||||||
|
limit = HOME_RECENT_DEBUT_CREATOR_LIMIT,
|
||||||
|
memberId = member?.id,
|
||||||
|
includeAdultContents = includeAdult
|
||||||
|
)
|
||||||
|
.map { it.toItem() },
|
||||||
|
firstAudioContents = queryService.findFirstAudioContents(
|
||||||
|
now,
|
||||||
|
limit = HOME_FIRST_AUDIO_CONTENT_LIMIT,
|
||||||
|
memberId = member?.id,
|
||||||
|
includeAdultContents = includeAdult
|
||||||
|
)
|
||||||
|
.map { it.toItem() },
|
||||||
|
aiCharacters = queryService.findAiCharacterRecommendations(limit = HOME_AI_CHARACTER_LIMIT).map { it.toItem() },
|
||||||
|
genreCreators = queryService.findGenreCreatorRecommendations(
|
||||||
|
memberId = member?.id,
|
||||||
|
includeAdultGenres = includeAdult,
|
||||||
|
genreLimit = HOME_GENRE_CREATOR_GENRE_LIMIT,
|
||||||
|
creatorLimit = HOME_GENRE_CREATOR_CREATOR_LIMIT
|
||||||
|
).map { it.toItem() },
|
||||||
|
cheerCreators = queryService.findCheerCreatorRecommendations(HOME_CHEER_CREATOR_LIMIT, member?.id)
|
||||||
|
.map { it.toCreatorItem() },
|
||||||
|
popularCommunities = queryService.findPopularCommunityRecommendations(
|
||||||
|
limit = HOME_POPULAR_COMMUNITY_LIMIT,
|
||||||
|
memberId = member?.id,
|
||||||
|
includeAdultCommunities = includeAdult
|
||||||
|
).map { it.toItem() }
|
||||||
)
|
)
|
||||||
.map { it.toItem() },
|
}.onSuccess { response ->
|
||||||
firstAudioContents = queryService.findFirstAudioContents(
|
log.info(
|
||||||
now,
|
"event=home_recommendations_query_success memberId={} elapsedMs={} emptySections={}",
|
||||||
limit = HOME_FIRST_AUDIO_CONTENT_LIMIT,
|
member?.id,
|
||||||
includeAdultContents = includeAdult
|
System.currentTimeMillis() - startedAt,
|
||||||
|
response.emptySections()
|
||||||
)
|
)
|
||||||
.map { it.toItem() },
|
}.onFailure { ex ->
|
||||||
aiCharacters = queryService.findAiCharacterRecommendations(limit = HOME_AI_CHARACTER_LIMIT).map { it.toItem() },
|
log.warn(
|
||||||
genreCreators = queryService.findGenreCreatorRecommendations(
|
"event=home_recommendations_query_failure memberId={} elapsedMs={} error={}",
|
||||||
memberId = member?.id,
|
member?.id,
|
||||||
includeAdultGenres = includeAdult,
|
System.currentTimeMillis() - startedAt,
|
||||||
genreLimit = HOME_GENRE_CREATOR_GENRE_LIMIT,
|
ex.message,
|
||||||
creatorLimit = HOME_GENRE_CREATOR_CREATOR_LIMIT
|
ex
|
||||||
).map { it.toItem() },
|
)
|
||||||
cheerCreators = queryService.findCheerCreatorRecommendations(HOME_CHEER_CREATOR_LIMIT)
|
}.getOrThrow()
|
||||||
.map { it.toCreatorItem() },
|
|
||||||
popularCommunities = queryService.findPopularCommunityRecommendations(
|
|
||||||
limit = HOME_POPULAR_COMMUNITY_LIMIT,
|
|
||||||
includeAdultCommunities = includeAdult
|
|
||||||
).map { it.toItem() }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLives(member: Member, page: Int, size: Int): HomeRecommendationPageResponse<HomeLiveItem> {
|
fun getLives(member: Member, page: Int, size: Int): HomeRecommendationPageResponse<HomeLiveItem> {
|
||||||
val fetched = queryService.findLiveRecommendations(
|
val startedAt = System.currentTimeMillis()
|
||||||
offset = page.toOffset(size),
|
return runCatching {
|
||||||
limit = size + 1,
|
val fetched = queryService.findLiveRecommendations(
|
||||||
includeAdultLives = resolveAdultVisibility(member)
|
offset = page.toOffset(size),
|
||||||
)
|
limit = size + 1,
|
||||||
return fetched.toPage(page, size) { it.toItem() }
|
memberId = member.id,
|
||||||
|
includeAdultLives = resolveAdultVisibility(member)
|
||||||
|
)
|
||||||
|
fetched.toPage(page, size) { it.toItem() }
|
||||||
|
}.onSuccess {
|
||||||
|
logPageSuccess("LIVE", member, page, size, it.items.size, System.currentTimeMillis() - startedAt)
|
||||||
|
}.onFailure { ex ->
|
||||||
|
logPageFailure("LIVE", member, page, size, startedAt, ex)
|
||||||
|
}.getOrThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getRecentDebutCreators(member: Member, page: Int, size: Int): HomeRecommendationPageResponse<HomeCreatorItem> {
|
fun getRecentDebutCreators(member: Member, page: Int, size: Int): HomeRecommendationPageResponse<HomeCreatorItem> {
|
||||||
val fetched = queryService.findRecentDebutCreators(
|
val startedAt = System.currentTimeMillis()
|
||||||
now = LocalDateTime.now(),
|
return runCatching {
|
||||||
offset = page.toOffset(size),
|
val fetched = queryService.findRecentDebutCreators(
|
||||||
limit = size + 1,
|
now = LocalDateTime.now(),
|
||||||
includeAdultContents = resolveAdultVisibility(member)
|
offset = page.toOffset(size),
|
||||||
)
|
limit = size + 1,
|
||||||
return fetched.toPage(page, size) { it.toItem() }
|
memberId = member.id,
|
||||||
|
includeAdultContents = resolveAdultVisibility(member)
|
||||||
|
)
|
||||||
|
fetched.toPage(page, size) { it.toItem() }
|
||||||
|
}.onSuccess {
|
||||||
|
logPageSuccess("DEBUT_CREATOR", member, page, size, it.items.size, System.currentTimeMillis() - startedAt)
|
||||||
|
}.onFailure { ex ->
|
||||||
|
logPageFailure("DEBUT_CREATOR", member, page, size, startedAt, ex)
|
||||||
|
}.getOrThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFirstAudioContents(member: Member, page: Int, size: Int): HomeRecommendationPageResponse<HomeFirstAudioContentItem> {
|
fun getFirstAudioContents(member: Member, page: Int, size: Int): HomeRecommendationPageResponse<HomeFirstAudioContentItem> {
|
||||||
val fetched = queryService.findFirstAudioContents(
|
val startedAt = System.currentTimeMillis()
|
||||||
now = LocalDateTime.now(),
|
return runCatching {
|
||||||
offset = page.toOffset(size),
|
val fetched = queryService.findFirstAudioContents(
|
||||||
limit = size + 1,
|
now = LocalDateTime.now(),
|
||||||
includeAdultContents = resolveAdultVisibility(member)
|
offset = page.toOffset(size),
|
||||||
)
|
limit = size + 1,
|
||||||
return fetched.toPage(page, size) { it.toItem() }
|
memberId = member.id,
|
||||||
|
includeAdultContents = resolveAdultVisibility(member)
|
||||||
|
)
|
||||||
|
fetched.toPage(page, size) { it.toItem() }
|
||||||
|
}.onSuccess {
|
||||||
|
logPageSuccess("FIRST_AUDIO_CONTENT", member, page, size, it.items.size, System.currentTimeMillis() - startedAt)
|
||||||
|
}.onFailure { ex ->
|
||||||
|
logPageFailure("FIRST_AUDIO_CONTENT", member, page, size, startedAt, ex)
|
||||||
|
}.getOrThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAiCharacters(member: Member, page: Int, size: Int): HomeRecommendationPageResponse<HomeAiCharacterItem> {
|
fun getAiCharacters(member: Member, page: Int, size: Int): HomeRecommendationPageResponse<HomeAiCharacterItem> {
|
||||||
val fetched = queryService.findAiCharacterRecommendations(offset = page.toOffset(size), limit = size + 1)
|
val startedAt = System.currentTimeMillis()
|
||||||
return fetched.toPage(page, size) { it.toItem() }
|
return runCatching {
|
||||||
|
val fetched = queryService.findAiCharacterRecommendations(offset = page.toOffset(size), limit = size + 1)
|
||||||
|
fetched.toPage(page, size) { it.toItem() }
|
||||||
|
}.onSuccess {
|
||||||
|
logPageSuccess("AI_CHARACTER", member, page, size, it.items.size, System.currentTimeMillis() - startedAt)
|
||||||
|
}.onFailure { ex ->
|
||||||
|
logPageFailure("AI_CHARACTER", member, page, size, startedAt, ex)
|
||||||
|
}.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logPageSuccess(section: String, member: Member, page: Int, size: Int, itemCount: Int, elapsedMs: Long) {
|
||||||
|
log.info(
|
||||||
|
"event=home_recommendations_page_query_success section={} memberId={} page={} size={} itemCount={} elapsedMs={}",
|
||||||
|
section,
|
||||||
|
member.id,
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
itemCount,
|
||||||
|
elapsedMs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun logPageFailure(section: String, member: Member, page: Int, size: Int, startedAt: Long, ex: Throwable) {
|
||||||
|
log.warn(
|
||||||
|
"event=home_recommendations_page_query_failure section={} memberId={} page={} size={} elapsedMs={} error={}",
|
||||||
|
section,
|
||||||
|
member.id,
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
System.currentTimeMillis() - startedAt,
|
||||||
|
ex.message,
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun HomeRecommendationResponse.emptySections(): List<String> {
|
||||||
|
return buildList {
|
||||||
|
if (lives.isEmpty()) add("lives")
|
||||||
|
if (banners.isEmpty()) add("banners")
|
||||||
|
if (recentlyActiveCreators.isEmpty()) add("recentlyActiveCreators")
|
||||||
|
if (recentDebutCreators.isEmpty()) add("recentDebutCreators")
|
||||||
|
if (firstAudioContents.isEmpty()) add("firstAudioContents")
|
||||||
|
if (aiCharacters.isEmpty()) add("aiCharacters")
|
||||||
|
if (genreCreators.isEmpty()) add("genreCreators")
|
||||||
|
if (cheerCreators.isEmpty()) add("cheerCreators")
|
||||||
|
if (popularCommunities.isEmpty()) add("popularCommunities")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveAdultVisibility(member: Member?): Boolean {
|
private fun resolveAdultVisibility(member: Member?): Boolean {
|
||||||
|
|||||||
@@ -5,17 +5,26 @@ import kr.co.vividnext.sodalive.member.Member
|
|||||||
import kr.co.vividnext.sodalive.member.MemberAdapter
|
import kr.co.vividnext.sodalive.member.MemberAdapter
|
||||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||||
import kr.co.vividnext.sodalive.member.MemberRole
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreference
|
||||||
|
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||||
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
|
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
|
||||||
import kr.co.vividnext.sodalive.member.following.CreatorFollowingRepository
|
import kr.co.vividnext.sodalive.member.following.CreatorFollowingRepository
|
||||||
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||||
|
import kr.co.vividnext.sodalive.v2.api.home.application.HomeRecommendationFacade
|
||||||
|
import kr.co.vividnext.sodalive.v2.recommend.application.HomeRecommendationQueryService
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
|
import org.mockito.Mockito
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||||
import org.springframework.boot.test.context.SpringBootTest
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.boot.test.system.CapturedOutput
|
||||||
|
import org.springframework.boot.test.system.OutputCaptureExtension
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
||||||
import org.springframework.test.context.ContextConfiguration
|
import org.springframework.test.context.ContextConfiguration
|
||||||
@@ -32,6 +41,7 @@ import javax.persistence.EntityManager
|
|||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
@Transactional
|
@Transactional
|
||||||
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
|
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
|
||||||
|
@ExtendWith(OutputCaptureExtension::class)
|
||||||
class HomeRecommendationControllerTest @Autowired constructor(
|
class HomeRecommendationControllerTest @Autowired constructor(
|
||||||
private val mockMvc: MockMvc,
|
private val mockMvc: MockMvc,
|
||||||
private val memberRepository: MemberRepository,
|
private val memberRepository: MemberRepository,
|
||||||
@@ -197,7 +207,7 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("메인 홈 통합 조회는 비회원도 호출 가능하고 섹션별 빈 배열을 포함해 성공 응답한다")
|
@DisplayName("메인 홈 통합 조회는 비회원도 호출 가능하고 섹션별 빈 배열을 포함해 성공 응답한다")
|
||||||
fun shouldReturnHomeRecommendationsForAnonymous() {
|
fun shouldReturnHomeRecommendationsForAnonymous(output: CapturedOutput) {
|
||||||
mockMvc.perform(get("/api/v2/home/recommendations"))
|
mockMvc.perform(get("/api/v2/home/recommendations"))
|
||||||
.andExpect(status().isOk)
|
.andExpect(status().isOk)
|
||||||
.andExpect(jsonPath("$.success").value(true))
|
.andExpect(jsonPath("$.success").value(true))
|
||||||
@@ -210,6 +220,9 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
|||||||
.andExpect(jsonPath("$.data.genreCreators").isArray)
|
.andExpect(jsonPath("$.data.genreCreators").isArray)
|
||||||
.andExpect(jsonPath("$.data.cheerCreators").isArray)
|
.andExpect(jsonPath("$.data.cheerCreators").isArray)
|
||||||
.andExpect(jsonPath("$.data.popularCommunities").isArray)
|
.andExpect(jsonPath("$.data.popularCommunities").isArray)
|
||||||
|
|
||||||
|
assertTrue(output.out.contains("event=home_recommendations_query_success"))
|
||||||
|
assertTrue(output.out.contains("emptySections="))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -227,9 +240,27 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
|||||||
.andExpect(jsonPath("$.data.lives").isArray)
|
.andExpect(jsonPath("$.data.lives").isArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("메인 홈 통합 조회 실패는 응답 시간과 함께 로그로 관측된다")
|
||||||
|
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))
|
||||||
|
.thenThrow(IllegalStateException("home query failed"))
|
||||||
|
|
||||||
|
val exception = assertThrows(IllegalStateException::class.java) {
|
||||||
|
facade.getHomeRecommendations(member = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("home query failed", exception.message)
|
||||||
|
assertTrue(output.out.contains("event=home_recommendations_query_failure"))
|
||||||
|
assertTrue(output.out.contains("memberId=null"))
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("라이브 전체보기는 page/size를 포함한 페이징 응답 형식으로 반환한다")
|
@DisplayName("라이브 전체보기는 page/size를 포함한 페이징 응답 형식으로 반환한다")
|
||||||
fun shouldReturnPagedLives() {
|
fun shouldReturnPagedLives(output: CapturedOutput) {
|
||||||
val member = saveMember("paged-live-viewer", MemberRole.USER)
|
val member = saveMember("paged-live-viewer", MemberRole.USER)
|
||||||
entityManager.flush()
|
entityManager.flush()
|
||||||
entityManager.clear()
|
entityManager.clear()
|
||||||
@@ -241,6 +272,74 @@ class HomeRecommendationControllerTest @Autowired constructor(
|
|||||||
.andExpect(jsonPath("$.data.page").value(0))
|
.andExpect(jsonPath("$.data.page").value(0))
|
||||||
.andExpect(jsonPath("$.data.size").value(20))
|
.andExpect(jsonPath("$.data.size").value(20))
|
||||||
.andExpect(jsonPath("$.data.hasNext").value(false))
|
.andExpect(jsonPath("$.data.hasNext").value(false))
|
||||||
|
|
||||||
|
assertTrue(output.out.contains("event=home_recommendations_page_query_success"))
|
||||||
|
assertTrue(output.out.contains("section=LIVE"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("세부 전체보기 조회 실패는 섹션과 응답 시간과 함께 로그로 관측된다")
|
||||||
|
fun shouldLogHomeRecommendationPageFailure(output: CapturedOutput) {
|
||||||
|
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")
|
||||||
|
Mockito.`when`(preferenceService.initializeDefaultPreference(member)).thenReturn(MemberContentPreference())
|
||||||
|
Mockito.`when`(
|
||||||
|
failingQueryService.findLiveRecommendations(
|
||||||
|
offset = 0,
|
||||||
|
limit = 21,
|
||||||
|
memberId = member.id,
|
||||||
|
includeAdultLives = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.thenThrow(IllegalStateException("page query failed"))
|
||||||
|
|
||||||
|
val exception = assertThrows(IllegalStateException::class.java) {
|
||||||
|
facade.getLives(member, page = 0, size = 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("page query failed", exception.message)
|
||||||
|
assertTrue(output.out.contains("event=home_recommendations_page_query_failure"))
|
||||||
|
assertTrue(output.out.contains("section=LIVE"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("나머지 세부 전체보기 조회 실패도 섹션과 응답 시간과 함께 로그로 관측된다")
|
||||||
|
fun shouldLogOtherHomeRecommendationPageFailures(output: CapturedOutput) {
|
||||||
|
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")
|
||||||
|
Mockito.`when`(preferenceService.initializeDefaultPreference(member)).thenReturn(MemberContentPreference())
|
||||||
|
Mockito.`when`(
|
||||||
|
failingQueryService.findRecentDebutCreators(
|
||||||
|
now = Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.MIN,
|
||||||
|
offset = Mockito.eq(0),
|
||||||
|
limit = Mockito.eq(21),
|
||||||
|
memberId = Mockito.eq(member.id),
|
||||||
|
includeAdultContents = Mockito.eq(false)
|
||||||
|
)
|
||||||
|
).thenThrow(IllegalStateException("debut page failed"))
|
||||||
|
Mockito.`when`(
|
||||||
|
failingQueryService.findFirstAudioContents(
|
||||||
|
now = Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.MIN,
|
||||||
|
offset = Mockito.eq(0),
|
||||||
|
limit = Mockito.eq(21),
|
||||||
|
memberId = Mockito.eq(member.id),
|
||||||
|
includeAdultContents = Mockito.eq(false)
|
||||||
|
)
|
||||||
|
).thenThrow(IllegalStateException("first audio page failed"))
|
||||||
|
Mockito.`when`(failingQueryService.findAiCharacterRecommendations(offset = 0, limit = 21))
|
||||||
|
.thenThrow(IllegalStateException("ai page failed"))
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException::class.java) { facade.getRecentDebutCreators(member, page = 0, size = 20) }
|
||||||
|
assertThrows(IllegalStateException::class.java) { facade.getFirstAudioContents(member, page = 0, size = 20) }
|
||||||
|
assertThrows(IllegalStateException::class.java) { facade.getAiCharacters(member, page = 0, size = 20) }
|
||||||
|
|
||||||
|
assertTrue(output.out.contains("section=DEBUT_CREATOR"))
|
||||||
|
assertTrue(output.out.contains("section=FIRST_AUDIO_CONTENT"))
|
||||||
|
assertTrue(output.out.contains("section=AI_CHARACTER"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user