test #426
@@ -0,0 +1,72 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.api.content.overview.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||||
|
import kr.co.vividnext.sodalive.v2.api.content.overview.dto.ContentOverviewItemResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.api.content.overview.dto.ContentOverviewPageResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.api.content.overview.dto.ContentOverviewType
|
||||||
|
import kr.co.vividnext.sodalive.v2.common.domain.toCdnUrl
|
||||||
|
import kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryService
|
||||||
|
import kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryService
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ContentOverviewFacade(
|
||||||
|
private val audioRecommendationQueryService: AudioRecommendationQueryService,
|
||||||
|
private val homeRecommendationQueryService: HomeRecommendationQueryService,
|
||||||
|
private val memberContentPreferenceService: MemberContentPreferenceService,
|
||||||
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
|
private val cloudFrontHost: String,
|
||||||
|
private val queryPolicy: ContentOverviewQueryPolicy = ContentOverviewQueryPolicy()
|
||||||
|
) {
|
||||||
|
fun getContents(type: String?, page: Int?, size: Int?, member: Member): ContentOverviewPageResponse {
|
||||||
|
val resolvedType = queryPolicy.resolveType(type)
|
||||||
|
val resolvedPage = queryPolicy.createPage(page, size)
|
||||||
|
|
||||||
|
return when (resolvedType) {
|
||||||
|
ContentOverviewType.NEW_AND_HOT_AUDIO -> getNewAndHotContents(member, resolvedPage)
|
||||||
|
ContentOverviewType.FIRST_AUDIO_CONTENT -> getFirstAudioContents(member, resolvedPage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNewAndHotContents(member: Member, page: ContentOverviewPage): ContentOverviewPageResponse {
|
||||||
|
val fetched = audioRecommendationQueryService.findNewAndHotAudios(
|
||||||
|
member = member,
|
||||||
|
offset = page.offset,
|
||||||
|
limit = page.size + 1
|
||||||
|
)
|
||||||
|
return ContentOverviewPageResponse(
|
||||||
|
type = ContentOverviewType.NEW_AND_HOT_AUDIO,
|
||||||
|
items = queryPolicy.pageItems(fetched, page).map { ContentOverviewItemResponse.fromNewAndHot(it) },
|
||||||
|
page = page.page,
|
||||||
|
size = page.size,
|
||||||
|
hasNext = queryPolicy.hasNext(fetched, page)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFirstAudioContents(member: Member, page: ContentOverviewPage): ContentOverviewPageResponse {
|
||||||
|
val fetched = homeRecommendationQueryService.findFirstAudioContents(
|
||||||
|
now = LocalDateTime.now(),
|
||||||
|
offset = page.offset,
|
||||||
|
limit = page.size + 1,
|
||||||
|
memberId = member.id,
|
||||||
|
includeAdultContents = memberContentPreferenceService.canViewAdultContent(member)
|
||||||
|
)
|
||||||
|
return ContentOverviewPageResponse(
|
||||||
|
type = ContentOverviewType.FIRST_AUDIO_CONTENT,
|
||||||
|
items = queryPolicy.pageItems(fetched, page).map {
|
||||||
|
ContentOverviewItemResponse.fromFirstAudioContent(
|
||||||
|
audio = it,
|
||||||
|
coverImage = it.coverImage.toCdnUrl(cloudFrontHost),
|
||||||
|
isAdult = it.isAdult,
|
||||||
|
isOriginalSeries = it.isOriginalSeries
|
||||||
|
)
|
||||||
|
},
|
||||||
|
page = page.page,
|
||||||
|
size = page.size,
|
||||||
|
hasNext = queryPolicy.hasNext(fetched, page)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.api.content.overview.application
|
||||||
|
|
||||||
|
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.api.content.overview.dto.ContentOverviewType
|
||||||
|
import kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryService
|
||||||
|
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioCard
|
||||||
|
import kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryService
|
||||||
|
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeFirstAudioContentRecord
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
class ContentOverviewFacadeTest {
|
||||||
|
private val audioRecommendationQueryService = Mockito.mock(AudioRecommendationQueryService::class.java)
|
||||||
|
private val homeRecommendationQueryService = Mockito.mock(HomeRecommendationQueryService::class.java)
|
||||||
|
private val memberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
|
||||||
|
private val facade = ContentOverviewFacade(
|
||||||
|
audioRecommendationQueryService = audioRecommendationQueryService,
|
||||||
|
homeRecommendationQueryService = homeRecommendationQueryService,
|
||||||
|
memberContentPreferenceService = memberContentPreferenceService,
|
||||||
|
cloudFrontHost = "https://cdn.test",
|
||||||
|
queryPolicy = ContentOverviewQueryPolicy()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("New & Hot 전체보기는 size + 1 조회 결과를 공통 페이지 응답으로 변환한다")
|
||||||
|
fun shouldReturnNewAndHotPage() {
|
||||||
|
val member = member(id = 10L)
|
||||||
|
Mockito.doReturn((1L..21L).map { audioCard(it) }).`when`(audioRecommendationQueryService)
|
||||||
|
.findNewAndHotAudios(member, offset = 0L, limit = 21)
|
||||||
|
|
||||||
|
val response = facade.getContents("NEW_AND_HOT_AUDIO", page = 0, size = 20, member = member)
|
||||||
|
|
||||||
|
assertEquals(ContentOverviewType.NEW_AND_HOT_AUDIO, response.type)
|
||||||
|
assertEquals((1L..20L).toList(), response.items.map { it.contentId })
|
||||||
|
assertEquals("https://cdn.test/audio1.png", response.items[0].coverImage)
|
||||||
|
assertEquals(0, response.page)
|
||||||
|
assertEquals(20, response.size)
|
||||||
|
assertEquals(true, response.hasNext)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("첫 번째 오디오 콘텐츠 전체보기는 adult visibility와 offset을 반영해 조회한다")
|
||||||
|
fun shouldReturnFirstAudioContentPage() {
|
||||||
|
val member = member(id = 10L)
|
||||||
|
Mockito.doReturn(true).`when`(memberContentPreferenceService).canViewAdultContent(member)
|
||||||
|
Mockito.doReturn(listOf(firstAudio(1L), firstAudio(2L))).`when`(homeRecommendationQueryService)
|
||||||
|
.findFirstAudioContents(
|
||||||
|
anyLocalDateTime(),
|
||||||
|
eqValue(20L),
|
||||||
|
eqValue(21),
|
||||||
|
eqValue(member.id),
|
||||||
|
eqValue(true)
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = facade.getContents("FIRST_AUDIO_CONTENT", page = 1, size = 20, member = member)
|
||||||
|
|
||||||
|
assertEquals(ContentOverviewType.FIRST_AUDIO_CONTENT, response.type)
|
||||||
|
assertEquals(listOf(1L, 2L), response.items.map { it.contentId })
|
||||||
|
assertEquals("https://cdn.test/cover/audio1.png", response.items[0].coverImage)
|
||||||
|
assertEquals(true, response.items[0].isFirstContent)
|
||||||
|
assertEquals(false, response.hasNext)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun member(id: Long): Member {
|
||||||
|
return Member(
|
||||||
|
email = "viewer$id@test.com",
|
||||||
|
password = "password",
|
||||||
|
nickname = "viewer$id",
|
||||||
|
role = MemberRole.USER
|
||||||
|
).apply {
|
||||||
|
this.id = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun audioCard(id: Long): AudioCard {
|
||||||
|
return AudioCard(
|
||||||
|
audioContentId = id,
|
||||||
|
title = "audio$id",
|
||||||
|
duration = "00:01",
|
||||||
|
imageUrl = "https://cdn.test/audio$id.png",
|
||||||
|
price = id.toInt(),
|
||||||
|
isAdult = false,
|
||||||
|
isPointAvailable = true,
|
||||||
|
isFirstContent = true,
|
||||||
|
isOriginalSeries = false,
|
||||||
|
creatorNickname = "creator$id"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun anyLocalDateTime(): LocalDateTime {
|
||||||
|
return Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.of(2026, 6, 27, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> eqValue(value: T): T {
|
||||||
|
return Mockito.eq(value) ?: value
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun firstAudio(id: Long): HomeFirstAudioContentRecord {
|
||||||
|
return HomeFirstAudioContentRecord(
|
||||||
|
contentId = id,
|
||||||
|
creatorId = id + 100,
|
||||||
|
creatorNickname = "creator$id",
|
||||||
|
creatorProfileImage = null,
|
||||||
|
title = "first audio$id",
|
||||||
|
price = id.toInt(),
|
||||||
|
coverImage = "cover/audio$id.png",
|
||||||
|
isPointAvailable = true,
|
||||||
|
isAdult = false,
|
||||||
|
isOriginalSeries = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user