feat(creator): 채널 라이브 탭 조회 서비스를 추가한다
This commit is contained in:
@@ -0,0 +1,137 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.live.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
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
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||||
|
import kr.co.vividnext.sodalive.member.contentpreference.isAdultVisibleByPolicy
|
||||||
|
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelAudioContent
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLive
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLiveReplayQueryPolicy
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLiveTab
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelAudioContentRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelCreatorRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelLiveQueryPort
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelLiveRecord
|
||||||
|
import org.springframework.beans.factory.ObjectProvider
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
class CreatorChannelLiveQueryService(
|
||||||
|
private val queryPortProvider: ObjectProvider<CreatorChannelLiveQueryPort>,
|
||||||
|
private val queryPolicy: CreatorChannelLiveReplayQueryPolicy,
|
||||||
|
private val memberContentPreferenceService: MemberContentPreferenceService,
|
||||||
|
private val messageSource: SodaMessageSource,
|
||||||
|
private val langContext: LangContext,
|
||||||
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
|
private val cloudFrontHost: String
|
||||||
|
) {
|
||||||
|
fun getLiveTab(
|
||||||
|
creatorId: Long,
|
||||||
|
viewer: Member,
|
||||||
|
sort: ContentSort,
|
||||||
|
page: Int,
|
||||||
|
size: Int,
|
||||||
|
now: LocalDateTime = LocalDateTime.now()
|
||||||
|
): CreatorChannelLiveTab {
|
||||||
|
val livePage = queryPolicy.createPage(page, size)
|
||||||
|
val queryPort = queryPortProvider.getObject()
|
||||||
|
val viewerId = viewer.id!!
|
||||||
|
val creator = queryPort.findCreator(creatorId, viewerId)
|
||||||
|
?: throw SodaException(messageKey = "member.validation.user_not_found")
|
||||||
|
|
||||||
|
if (queryPort.existsBlockedBetween(viewerId, creatorId)) {
|
||||||
|
val messageTemplate = messageSource
|
||||||
|
.getMessage("explorer.creator.blocked_access", langContext.lang)
|
||||||
|
.orEmpty()
|
||||||
|
throw SodaException(message = String.format(messageTemplate, creator.nickname))
|
||||||
|
}
|
||||||
|
|
||||||
|
validateCreatorRole(creator)
|
||||||
|
|
||||||
|
val preference = memberContentPreferenceService.getStoredPreference(viewer)
|
||||||
|
val canViewAdultContent = isAdultVisibleByPolicy(viewer, preference.isAdultContentVisible)
|
||||||
|
val isViewerCreator = viewerId == creatorId
|
||||||
|
val effectiveViewerGender = viewer.effectiveGender()
|
||||||
|
val fetchedContents = queryPort.findLiveReplayAudioContents(
|
||||||
|
creatorId = creatorId,
|
||||||
|
viewerId = viewerId,
|
||||||
|
now = now,
|
||||||
|
canViewAdultContent = canViewAdultContent,
|
||||||
|
sort = sort,
|
||||||
|
offset = livePage.offset,
|
||||||
|
limit = livePage.fetchLimit
|
||||||
|
)
|
||||||
|
|
||||||
|
return CreatorChannelLiveTab(
|
||||||
|
liveReplayContentCount = queryPort.countLiveReplayAudioContents(
|
||||||
|
creatorId = creatorId,
|
||||||
|
now = now,
|
||||||
|
canViewAdultContent = canViewAdultContent
|
||||||
|
),
|
||||||
|
currentLive = queryPort.findCurrentLive(
|
||||||
|
creatorId = creatorId,
|
||||||
|
now = now,
|
||||||
|
canViewAdultContent = canViewAdultContent,
|
||||||
|
viewerId = viewerId,
|
||||||
|
isViewerCreator = isViewerCreator,
|
||||||
|
effectiveViewerGender = effectiveViewerGender
|
||||||
|
)?.toDomain(),
|
||||||
|
liveReplayContents = queryPolicy.limitItems(fetchedContents, livePage).map { it.toDomain() },
|
||||||
|
sort = sort,
|
||||||
|
page = livePage,
|
||||||
|
hasNext = queryPolicy.hasNext(fetchedContents, livePage)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateCreatorRole(creator: CreatorChannelCreatorRecord) {
|
||||||
|
when (creator.role) {
|
||||||
|
MemberRole.CREATOR -> return
|
||||||
|
else -> throw SodaException(messageKey = "member.validation.creator_not_found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Member.effectiveGender(): Gender {
|
||||||
|
auth?.let { return if (it.gender == 1) Gender.MALE else Gender.FEMALE }
|
||||||
|
return gender
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CreatorChannelLiveRecord.toDomain() = CreatorChannelLive(
|
||||||
|
liveId = liveId,
|
||||||
|
title = title,
|
||||||
|
coverImageUrl = coverImagePath.toCdnUrl(),
|
||||||
|
beginDateTime = beginDateTime,
|
||||||
|
price = price,
|
||||||
|
isAdult = isAdult
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun CreatorChannelAudioContentRecord.toDomain() = CreatorChannelAudioContent(
|
||||||
|
audioContentId = audioContentId,
|
||||||
|
title = title,
|
||||||
|
duration = duration,
|
||||||
|
imageUrl = imagePath.toCdnUrl(),
|
||||||
|
price = price,
|
||||||
|
isAdult = isAdult,
|
||||||
|
isPointAvailable = isPointAvailable,
|
||||||
|
isFirstContent = isFirstContent,
|
||||||
|
publishedAt = publishedAt,
|
||||||
|
seriesName = seriesName,
|
||||||
|
isOriginalSeries = isOriginalSeries,
|
||||||
|
isOwned = isOwned,
|
||||||
|
isRented = isRented
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun String?.toCdnUrl(): String? {
|
||||||
|
if (isNullOrBlank()) return null
|
||||||
|
if (startsWith("https://") || startsWith("http://")) return this
|
||||||
|
return "$cloudFrontHost/$this"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.live.port.out
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.member.Gender
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
interface CreatorChannelLiveQueryPort {
|
||||||
|
fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCreatorRecord?
|
||||||
|
|
||||||
|
fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean
|
||||||
|
|
||||||
|
fun findCurrentLive(
|
||||||
|
creatorId: Long,
|
||||||
|
now: LocalDateTime,
|
||||||
|
canViewAdultContent: Boolean,
|
||||||
|
viewerId: Long?,
|
||||||
|
isViewerCreator: Boolean,
|
||||||
|
effectiveViewerGender: Gender?
|
||||||
|
): CreatorChannelLiveRecord?
|
||||||
|
|
||||||
|
fun countLiveReplayAudioContents(
|
||||||
|
creatorId: Long,
|
||||||
|
now: LocalDateTime,
|
||||||
|
canViewAdultContent: Boolean
|
||||||
|
): Int
|
||||||
|
|
||||||
|
fun findLiveReplayAudioContents(
|
||||||
|
creatorId: Long,
|
||||||
|
viewerId: Long?,
|
||||||
|
now: LocalDateTime,
|
||||||
|
canViewAdultContent: Boolean,
|
||||||
|
sort: ContentSort,
|
||||||
|
offset: Long,
|
||||||
|
limit: Int
|
||||||
|
): List<CreatorChannelAudioContentRecord>
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CreatorChannelCreatorRecord(
|
||||||
|
val creatorId: Long,
|
||||||
|
val role: MemberRole,
|
||||||
|
val nickname: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelLiveRecord(
|
||||||
|
val liveId: Long,
|
||||||
|
val title: String,
|
||||||
|
val coverImagePath: String?,
|
||||||
|
val beginDateTime: LocalDateTime,
|
||||||
|
val price: Int,
|
||||||
|
val isAdult: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreatorChannelAudioContentRecord(
|
||||||
|
val audioContentId: Long,
|
||||||
|
val title: String,
|
||||||
|
val duration: String?,
|
||||||
|
val imagePath: String?,
|
||||||
|
val price: Int,
|
||||||
|
val isAdult: Boolean,
|
||||||
|
val isPointAvailable: Boolean,
|
||||||
|
val isFirstContent: Boolean,
|
||||||
|
val publishedAt: LocalDateTime,
|
||||||
|
val seriesName: String?,
|
||||||
|
val isOriginalSeries: Boolean?,
|
||||||
|
val isOwned: Boolean,
|
||||||
|
val isRented: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.live.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.content.ContentType
|
||||||
|
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
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberProvider
|
||||||
|
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.contentpreference.ViewerContentPreference
|
||||||
|
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelLiveReplayQueryPolicy
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelAudioContentRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelCreatorRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelLiveQueryPort
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.live.port.out.CreatorChannelLiveRecord
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.springframework.beans.factory.ObjectProvider
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
class CreatorChannelLiveQueryServiceTest {
|
||||||
|
@Test
|
||||||
|
@DisplayName("라이브 탭 서비스는 검증 후 현재 라이브와 다시듣기 조회에 필요한 정책 컨텍스트를 전달한다")
|
||||||
|
fun shouldPassLiveTabQueryContextToPort() {
|
||||||
|
val port = FakeCreatorChannelLiveQueryPort()
|
||||||
|
val service = createService(port, canViewAdultContent = false)
|
||||||
|
val viewer = createMember(id = 10L, gender = Gender.FEMALE, authGender = 1)
|
||||||
|
val now = LocalDateTime.of(2026, 6, 17, 10, 0)
|
||||||
|
|
||||||
|
val tab = service.getLiveTab(
|
||||||
|
creatorId = 1L,
|
||||||
|
viewer = viewer,
|
||||||
|
sort = ContentSort.LATEST,
|
||||||
|
page = 0,
|
||||||
|
size = 20,
|
||||||
|
now = now
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(1L, port.findCreatorCreatorId)
|
||||||
|
assertEquals(10L, port.findCreatorViewerId)
|
||||||
|
assertEquals(10L, port.existsBlockedViewerId)
|
||||||
|
assertEquals(1L, port.existsBlockedCreatorId)
|
||||||
|
assertEquals(10L, port.currentLiveViewerId)
|
||||||
|
assertFalse(port.currentLiveIsViewerCreator == true)
|
||||||
|
assertEquals(Gender.MALE, port.currentLiveEffectiveViewerGender)
|
||||||
|
assertEquals(false, port.currentLiveCanViewAdultContent)
|
||||||
|
assertEquals(false, port.countCanViewAdultContent)
|
||||||
|
assertEquals(false, port.listCanViewAdultContent)
|
||||||
|
assertEquals(ContentSort.LATEST, port.listSort)
|
||||||
|
assertEquals(0L, port.listOffset)
|
||||||
|
assertEquals(21, port.listLimit)
|
||||||
|
assertEquals("https://cdn.test/live.png", tab.currentLive?.coverImageUrl)
|
||||||
|
assertEquals("https://cdn.test/audio/1.png", tab.liveReplayContents.first().imageUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("라이브 탭 서비스는 size + 1개 조회 결과를 응답 size로 제한하고 hasNext를 true로 반환한다")
|
||||||
|
fun shouldAssembleLiveTabWithHasNextWhenFetchedMoreThanRequestedSize() {
|
||||||
|
val port = FakeCreatorChannelLiveQueryPort().apply {
|
||||||
|
liveReplayContents = (1L..21L).map { audioContentRecord(it) }
|
||||||
|
}
|
||||||
|
val service = createService(port)
|
||||||
|
val viewer = createMember(id = 10L)
|
||||||
|
|
||||||
|
val tab = service.getLiveTab(
|
||||||
|
creatorId = 1L,
|
||||||
|
viewer = viewer,
|
||||||
|
sort = ContentSort.LATEST,
|
||||||
|
page = 0,
|
||||||
|
size = 20,
|
||||||
|
now = LocalDateTime.of(2026, 6, 17, 10, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(30, tab.liveReplayContentCount)
|
||||||
|
assertEquals(20, tab.liveReplayContents.size)
|
||||||
|
assertEquals((1L..20L).toList(), tab.liveReplayContents.map { it.audioContentId })
|
||||||
|
assertTrue(tab.hasNext)
|
||||||
|
assertEquals(ContentSort.LATEST, tab.sort)
|
||||||
|
assertEquals(0, tab.page.page)
|
||||||
|
assertEquals(20, tab.page.size)
|
||||||
|
assertTrue(tab.liveReplayContents.first().isOwned)
|
||||||
|
assertFalse(tab.liveReplayContents.first().isRented)
|
||||||
|
assertTrue(tab.liveReplayContents[1].isRented)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("라이브 탭 서비스는 빈 page에서도 count와 요청 page 정보를 유지한다")
|
||||||
|
fun shouldKeepCountAndPageWhenReplayContentsAreEmpty() {
|
||||||
|
val port = FakeCreatorChannelLiveQueryPort().apply {
|
||||||
|
liveReplayContents = emptyList()
|
||||||
|
currentLive = null
|
||||||
|
}
|
||||||
|
val service = createService(port)
|
||||||
|
val viewer = createMember(id = 10L)
|
||||||
|
|
||||||
|
val tab = service.getLiveTab(
|
||||||
|
creatorId = 1L,
|
||||||
|
viewer = viewer,
|
||||||
|
sort = ContentSort.PRICE_LOW,
|
||||||
|
page = 2,
|
||||||
|
size = 20,
|
||||||
|
now = LocalDateTime.of(2026, 6, 17, 10, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(30, tab.liveReplayContentCount)
|
||||||
|
assertNull(tab.currentLive)
|
||||||
|
assertEquals(emptyList<Any>(), tab.liveReplayContents)
|
||||||
|
assertFalse(tab.hasNext)
|
||||||
|
assertEquals(ContentSort.PRICE_LOW, tab.sort)
|
||||||
|
assertEquals(2, tab.page.page)
|
||||||
|
assertEquals(20, tab.page.size)
|
||||||
|
assertEquals(40L, port.listOffset)
|
||||||
|
assertEquals(21, port.listLimit)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("creatorId에 해당하는 회원이 없으면 user_not_found 예외를 던진다")
|
||||||
|
fun shouldThrowUserNotFoundWhenCreatorMemberDoesNotExist() {
|
||||||
|
val port = FakeCreatorChannelLiveQueryPort().apply {
|
||||||
|
creator = null
|
||||||
|
}
|
||||||
|
val service = createService(port)
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("member.validation.user_not_found", exception.messageKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("대상 회원 role이 CREATOR가 아니면 creator_not_found 예외를 던진다")
|
||||||
|
fun shouldThrowCreatorNotFoundWhenMemberIsNotCreator() {
|
||||||
|
val port = FakeCreatorChannelLiveQueryPort().apply {
|
||||||
|
creator = creator?.copy(role = MemberRole.USER)
|
||||||
|
}
|
||||||
|
val service = createService(port)
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("member.validation.creator_not_found", exception.messageKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("차단 관계가 있으면 대상 회원이 크리에이터가 아니어도 접근 차단 예외를 먼저 던진다")
|
||||||
|
fun shouldThrowBlockedAccessBeforeCreatorNotFoundWhenViewerAndTargetAreBlocked() {
|
||||||
|
val port = FakeCreatorChannelLiveQueryPort().apply {
|
||||||
|
creator = creator?.copy(role = MemberRole.USER)
|
||||||
|
blocked = true
|
||||||
|
}
|
||||||
|
val service = createService(port)
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNull(exception.messageKey)
|
||||||
|
assertEquals("creator님의 요청으로 채널 접근이 제한됩니다.", exception.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("잘못된 page 요청이면 invalid request 예외를 던진다")
|
||||||
|
fun shouldThrowInvalidRequestWhenPageIsNegative() {
|
||||||
|
val service = createServiceWithMissingPort()
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("common.error.invalid_request", exception.messageKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("잘못된 size 요청이면 port 조회 전에 invalid request 예외를 던진다")
|
||||||
|
fun shouldThrowInvalidRequestBeforePortLookupWhenSizeIsOutOfRange() {
|
||||||
|
val service = createServiceWithMissingPort()
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("common.error.invalid_request", exception.messageKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createService(
|
||||||
|
port: FakeCreatorChannelLiveQueryPort,
|
||||||
|
canViewAdultContent: Boolean = true
|
||||||
|
): CreatorChannelLiveQueryService {
|
||||||
|
return createService(
|
||||||
|
portProvider = FixedCreatorChannelLiveQueryPortProvider(port),
|
||||||
|
canViewAdultContent = canViewAdultContent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createServiceWithMissingPort(): CreatorChannelLiveQueryService {
|
||||||
|
return createService(portProvider = MissingCreatorChannelLiveQueryPortProvider())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createService(
|
||||||
|
portProvider: ObjectProvider<CreatorChannelLiveQueryPort>,
|
||||||
|
canViewAdultContent: Boolean = true
|
||||||
|
): CreatorChannelLiveQueryService {
|
||||||
|
val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
|
||||||
|
Mockito.`when`(
|
||||||
|
preferenceService.getStoredPreference(Mockito.any(Member::class.java) ?: createMember(id = 0L))
|
||||||
|
).thenReturn(
|
||||||
|
ViewerContentPreference(
|
||||||
|
countryCode = "US",
|
||||||
|
isAdultContentVisible = canViewAdultContent,
|
||||||
|
contentType = ContentType.ALL,
|
||||||
|
isAdult = 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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMember(
|
||||||
|
id: Long,
|
||||||
|
gender: Gender = Gender.NONE,
|
||||||
|
authGender: Int? = null
|
||||||
|
): Member {
|
||||||
|
val member = Member(
|
||||||
|
email = "member$id@test.com",
|
||||||
|
password = "password",
|
||||||
|
nickname = "member$id",
|
||||||
|
provider = MemberProvider.EMAIL,
|
||||||
|
gender = gender
|
||||||
|
)
|
||||||
|
member.id = id
|
||||||
|
authGender?.let {
|
||||||
|
Auth(
|
||||||
|
name = "name",
|
||||||
|
birth = "19900101",
|
||||||
|
uniqueCi = "ci$id",
|
||||||
|
di = "di$id",
|
||||||
|
gender = it
|
||||||
|
).member = member
|
||||||
|
}
|
||||||
|
return member
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FixedCreatorChannelLiveQueryPortProvider(
|
||||||
|
private val port: CreatorChannelLiveQueryPort
|
||||||
|
) : ObjectProvider<CreatorChannelLiveQueryPort> {
|
||||||
|
override fun getObject(vararg args: Any?): CreatorChannelLiveQueryPort = port
|
||||||
|
|
||||||
|
override fun getIfAvailable(): CreatorChannelLiveQueryPort = port
|
||||||
|
|
||||||
|
override fun getIfUnique(): CreatorChannelLiveQueryPort = port
|
||||||
|
|
||||||
|
override fun getObject(): CreatorChannelLiveQueryPort = port
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MissingCreatorChannelLiveQueryPortProvider : ObjectProvider<CreatorChannelLiveQueryPort> {
|
||||||
|
override fun getObject(vararg args: Any?): CreatorChannelLiveQueryPort {
|
||||||
|
throw IllegalStateException("port should not be resolved before page validation")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getIfAvailable(): CreatorChannelLiveQueryPort? = null
|
||||||
|
|
||||||
|
override fun getIfUnique(): CreatorChannelLiveQueryPort? = null
|
||||||
|
|
||||||
|
override fun getObject(): CreatorChannelLiveQueryPort {
|
||||||
|
throw IllegalStateException("port should not be resolved before page validation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FakeCreatorChannelLiveQueryPort : CreatorChannelLiveQueryPort {
|
||||||
|
var creator: CreatorChannelCreatorRecord? = CreatorChannelCreatorRecord(
|
||||||
|
creatorId = 1L,
|
||||||
|
role = MemberRole.CREATOR,
|
||||||
|
nickname = "creator"
|
||||||
|
)
|
||||||
|
var blocked = false
|
||||||
|
var currentLive: CreatorChannelLiveRecord? = CreatorChannelLiveRecord(
|
||||||
|
liveId = 101L,
|
||||||
|
title = "live",
|
||||||
|
coverImagePath = "live.png",
|
||||||
|
beginDateTime = LocalDateTime.of(2026, 6, 17, 9, 0),
|
||||||
|
price = 10,
|
||||||
|
isAdult = false
|
||||||
|
)
|
||||||
|
var liveReplayContentCount = 30
|
||||||
|
var liveReplayContents = (1L..21L).map { audioContentRecord(it) }
|
||||||
|
var findCreatorCreatorId: Long? = null
|
||||||
|
var findCreatorViewerId: Long? = null
|
||||||
|
var existsBlockedViewerId: Long? = null
|
||||||
|
var existsBlockedCreatorId: Long? = null
|
||||||
|
var currentLiveViewerId: Long? = null
|
||||||
|
var currentLiveIsViewerCreator: Boolean? = null
|
||||||
|
var currentLiveEffectiveViewerGender: Gender? = null
|
||||||
|
var currentLiveCanViewAdultContent: Boolean? = null
|
||||||
|
var countCanViewAdultContent: Boolean? = null
|
||||||
|
var listCanViewAdultContent: Boolean? = null
|
||||||
|
var listSort: ContentSort? = null
|
||||||
|
var listOffset: Long? = null
|
||||||
|
var listLimit: Int? = null
|
||||||
|
|
||||||
|
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCreatorRecord? {
|
||||||
|
findCreatorCreatorId = creatorId
|
||||||
|
findCreatorViewerId = viewerId
|
||||||
|
return creator
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean {
|
||||||
|
existsBlockedViewerId = viewerId
|
||||||
|
existsBlockedCreatorId = creatorId
|
||||||
|
return blocked
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findCurrentLive(
|
||||||
|
creatorId: Long,
|
||||||
|
now: LocalDateTime,
|
||||||
|
canViewAdultContent: Boolean,
|
||||||
|
viewerId: Long?,
|
||||||
|
isViewerCreator: Boolean,
|
||||||
|
effectiveViewerGender: Gender?
|
||||||
|
): CreatorChannelLiveRecord? {
|
||||||
|
currentLiveViewerId = viewerId
|
||||||
|
currentLiveIsViewerCreator = isViewerCreator
|
||||||
|
currentLiveEffectiveViewerGender = effectiveViewerGender
|
||||||
|
currentLiveCanViewAdultContent = canViewAdultContent
|
||||||
|
return currentLive
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun countLiveReplayAudioContents(
|
||||||
|
creatorId: Long,
|
||||||
|
now: LocalDateTime,
|
||||||
|
canViewAdultContent: Boolean
|
||||||
|
): Int {
|
||||||
|
countCanViewAdultContent = canViewAdultContent
|
||||||
|
return liveReplayContentCount
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findLiveReplayAudioContents(
|
||||||
|
creatorId: Long,
|
||||||
|
viewerId: Long?,
|
||||||
|
now: LocalDateTime,
|
||||||
|
canViewAdultContent: Boolean,
|
||||||
|
sort: ContentSort,
|
||||||
|
offset: Long,
|
||||||
|
limit: Int
|
||||||
|
): List<CreatorChannelAudioContentRecord> {
|
||||||
|
listCanViewAdultContent = canViewAdultContent
|
||||||
|
listSort = sort
|
||||||
|
listOffset = offset
|
||||||
|
listLimit = limit
|
||||||
|
return liveReplayContents
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun audioContentRecord(audioContentId: Long): CreatorChannelAudioContentRecord {
|
||||||
|
return CreatorChannelAudioContentRecord(
|
||||||
|
audioContentId = audioContentId,
|
||||||
|
title = "audio-$audioContentId",
|
||||||
|
duration = "00:10:00",
|
||||||
|
imagePath = "audio/$audioContentId.png",
|
||||||
|
price = 10,
|
||||||
|
isAdult = false,
|
||||||
|
isPointAvailable = true,
|
||||||
|
isFirstContent = audioContentId == 1L,
|
||||||
|
publishedAt = LocalDateTime.of(2026, 6, 16, 10, 0),
|
||||||
|
seriesName = "series",
|
||||||
|
isOriginalSeries = true,
|
||||||
|
isOwned = audioContentId == 1L,
|
||||||
|
isRented = audioContentId == 2L
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user