test #426

Merged
klaus merged 415 commits from test into main 2026-06-27 00:35:30 +00:00
3 changed files with 473 additions and 0 deletions
Showing only changes of commit 2bced956dc - Show all commits

View File

@@ -0,0 +1,171 @@
package kr.co.vividnext.sodalive.v2.content.all.application
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAll
import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllQueryPolicy
import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllType
import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentPage
import kr.co.vividnext.sodalive.v2.content.all.port.out.MainContentAllQueryPort
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
@Service
@Transactional(readOnly = true)
class MainContentAllQueryService(
private val queryPort: MainContentAllQueryPort,
private val memberContentPreferenceService: MemberContentPreferenceService,
private val queryPolicy: MainContentAllQueryPolicy = MainContentAllQueryPolicy(),
private val langContext: LangContext
) {
fun getContents(
type: String?,
sort: String?,
dayOfWeek: String?,
page: Int?,
size: Int?,
member: Member?
): MainContentAll {
val resolvedType = queryPolicy.resolveType(type)
val resolvedSort = queryPolicy.resolveSort(sort)
val resolvedDayOfWeek = queryPolicy.resolveDayOfWeek(resolvedType, dayOfWeek)
val resolvedPage = queryPolicy.createPage(page, size)
val now = LocalDateTime.now()
val memberId = member?.id
val canViewAdultContent = canViewAdultContent(member)
return when (resolvedType) {
MainContentAllType.AUDIO -> getAudioContents(
type = resolvedType,
sort = resolvedSort,
dayOfWeek = resolvedDayOfWeek,
page = resolvedPage,
memberId = memberId,
canViewAdultContent = canViewAdultContent,
now = now
)
MainContentAllType.FREE -> getAudioContents(
type = resolvedType,
sort = resolvedSort,
dayOfWeek = resolvedDayOfWeek,
page = resolvedPage,
memberId = memberId,
canViewAdultContent = canViewAdultContent,
now = now,
onlyFree = true
)
MainContentAllType.POINT -> getAudioContents(
type = resolvedType,
sort = resolvedSort,
dayOfWeek = resolvedDayOfWeek,
page = resolvedPage,
memberId = memberId,
canViewAdultContent = canViewAdultContent,
now = now,
onlyPointAvailable = true
)
MainContentAllType.SERIES -> getSeriesContents(
type = resolvedType,
sort = resolvedSort,
dayOfWeek = resolvedDayOfWeek,
page = resolvedPage,
memberId = memberId,
canViewAdultContent = canViewAdultContent,
now = now
)
MainContentAllType.ORIGINAL -> getSeriesContents(
type = resolvedType,
sort = resolvedSort,
dayOfWeek = null,
page = resolvedPage,
memberId = memberId,
canViewAdultContent = canViewAdultContent,
now = now,
onlyOriginal = true
)
}
}
private fun getAudioContents(
type: MainContentAllType,
sort: ContentSort,
dayOfWeek: SeriesPublishedDaysOfWeek?,
page: MainContentPage,
memberId: Long?,
canViewAdultContent: Boolean,
now: LocalDateTime,
onlyFree: Boolean = false,
onlyPointAvailable: Boolean = false
): MainContentAll {
val totalCount = queryPort.countAudios(memberId, canViewAdultContent, now, onlyFree, onlyPointAvailable)
val audios = queryPort.findAudios(
memberId = memberId,
canViewAdultContent = canViewAdultContent,
now = now,
sort = sort,
offset = page.offset,
limit = page.size + 1,
onlyFree = onlyFree,
onlyPointAvailable = onlyPointAvailable
)
return MainContentAll(
type = type,
totalCount = totalCount,
audios = queryPolicy.limitItems(audios, page),
series = emptyList(),
sort = sort,
dayOfWeek = dayOfWeek,
page = page,
hasNext = queryPolicy.hasNext(audios, page)
)
}
private fun getSeriesContents(
type: MainContentAllType,
sort: ContentSort,
dayOfWeek: SeriesPublishedDaysOfWeek?,
page: MainContentPage,
memberId: Long?,
canViewAdultContent: Boolean,
now: LocalDateTime,
onlyOriginal: Boolean = false
): MainContentAll {
val totalCount = queryPort.countSeries(memberId, canViewAdultContent, now, onlyOriginal, dayOfWeek)
val series = queryPort.findSeries(
memberId = memberId,
canViewAdultContent = canViewAdultContent,
now = now,
sort = sort,
offset = page.offset,
limit = page.size + 1,
onlyOriginal = onlyOriginal,
dayOfWeek = dayOfWeek,
locale = langContext.lang.code
)
return MainContentAll(
type = type,
totalCount = totalCount,
audios = emptyList(),
series = queryPolicy.limitItems(series, page),
sort = sort,
dayOfWeek = dayOfWeek,
page = page,
hasNext = queryPolicy.hasNext(series, page)
)
}
private fun canViewAdultContent(member: Member?): Boolean {
if (member == null) return false
return memberContentPreferenceService.canViewAdultContent(member)
}
}

View File

@@ -0,0 +1,48 @@
package kr.co.vividnext.sodalive.v2.content.all.port.out
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
import kr.co.vividnext.sodalive.v2.common.domain.ContentSort
import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllAudio
import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllSeries
import java.time.LocalDateTime
interface MainContentAllQueryPort {
fun countAudios(
memberId: Long?,
canViewAdultContent: Boolean,
now: LocalDateTime,
onlyFree: Boolean = false,
onlyPointAvailable: Boolean = false
): Int
fun findAudios(
memberId: Long?,
canViewAdultContent: Boolean,
now: LocalDateTime,
sort: ContentSort,
offset: Long,
limit: Int,
onlyFree: Boolean = false,
onlyPointAvailable: Boolean = false
): List<MainContentAllAudio>
fun countSeries(
memberId: Long?,
canViewAdultContent: Boolean,
now: LocalDateTime,
onlyOriginal: Boolean = false,
dayOfWeek: SeriesPublishedDaysOfWeek? = null
): Int
fun findSeries(
memberId: Long?,
canViewAdultContent: Boolean,
now: LocalDateTime,
sort: ContentSort,
offset: Long,
limit: Int,
onlyOriginal: Boolean = false,
dayOfWeek: SeriesPublishedDaysOfWeek? = null,
locale: String
): List<MainContentAllSeries>
}

View File

@@ -0,0 +1,254 @@
package kr.co.vividnext.sodalive.v2.content.all.application
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesPublishedDaysOfWeek
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
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.common.domain.ContentSort
import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllAudio
import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllQueryPolicy
import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllSeries
import kr.co.vividnext.sodalive.v2.content.all.domain.MainContentAllType
import kr.co.vividnext.sodalive.v2.content.all.port.out.MainContentAllQueryPort
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
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import java.time.LocalDateTime
class MainContentAllQueryServiceTest {
@Test
@DisplayName("AUDIO 타입은 audio port를 기본 필터로 호출한다")
fun shouldQueryAudiosForAudioType() {
val port = FakeMainContentAllQueryPort()
val service = createService(port)
val tab = service.getContents(type = "AUDIO", sort = "POPULAR", dayOfWeek = "MON", page = 1, size = 20, member = null)
assertEquals(MainContentAllType.AUDIO, tab.type)
assertEquals("audio", port.lastListKind)
assertEquals(ContentSort.POPULAR, port.lastSort)
assertEquals(20L, port.lastOffset)
assertEquals(21, port.lastLimit)
assertFalse(port.lastOnlyFree)
assertFalse(port.lastOnlyPointAvailable)
assertEquals(20, tab.audios.size)
assertTrue(tab.hasNext)
assertEquals(emptyList<MainContentAllSeries>(), tab.series)
assertEquals(null, tab.dayOfWeek)
}
@Test
@DisplayName("FREE 타입은 audio port를 무료 필터로 호출한다")
fun shouldQueryAudiosForFreeType() {
val port = FakeMainContentAllQueryPort()
val service = createService(port)
val tab = service.getContents(type = "FREE", sort = "LATEST", dayOfWeek = null, page = 0, size = 20, member = null)
assertEquals(MainContentAllType.FREE, tab.type)
assertEquals("audio", port.lastListKind)
assertTrue(port.lastOnlyFree)
assertFalse(port.lastOnlyPointAvailable)
assertEquals(21, port.lastLimit)
assertEquals(20, tab.audios.size)
assertTrue(tab.hasNext)
assertEquals(emptyList<MainContentAllSeries>(), tab.series)
}
@Test
@DisplayName("POINT 타입은 audio port를 포인트 사용 가능 필터로 호출한다")
fun shouldQueryAudiosForPointType() {
val port = FakeMainContentAllQueryPort()
val service = createService(port)
val tab = service.getContents(type = "POINT", sort = "PRICE_LOW", dayOfWeek = null, page = 0, size = 20, member = null)
assertEquals(MainContentAllType.POINT, tab.type)
assertEquals("audio", port.lastListKind)
assertEquals(ContentSort.PRICE_LOW, port.lastSort)
assertFalse(port.lastOnlyFree)
assertTrue(port.lastOnlyPointAvailable)
assertEquals(21, port.lastLimit)
assertEquals(20, tab.audios.size)
assertTrue(tab.hasNext)
assertEquals(emptyList<MainContentAllSeries>(), tab.series)
}
@Test
@DisplayName("SERIES는 요일을 전달하고 ORIGINAL은 original 필터와 dayOfWeek null을 전달한다")
fun shouldQuerySeriesByType() {
val seriesPort = FakeMainContentAllQueryPort()
val service = createService(seriesPort, lang = Lang.JA)
val seriesTab = service.getContents("SERIES", "POPULAR", "MON", 0, 20, null)
assertEquals(MainContentAllType.SERIES, seriesTab.type)
assertEquals("series", seriesPort.lastListKind)
assertEquals(SeriesPublishedDaysOfWeek.MON, seriesPort.lastDayOfWeek)
assertEquals("ja", seriesPort.lastLocale)
assertFalse(seriesPort.lastOnlyOriginal)
val originalPort = FakeMainContentAllQueryPort()
val originalService = createService(originalPort)
val originalTab = originalService.getContents("ORIGINAL", "POPULAR", "MON", 0, 20, null)
assertEquals(MainContentAllType.ORIGINAL, originalTab.type)
assertEquals("series", originalPort.lastListKind)
assertEquals(null, originalPort.lastDayOfWeek)
assertTrue(originalPort.lastOnlyOriginal)
}
@Test
@DisplayName("비회원은 성인 콘텐츠 비노출로 조회하고 회원은 preference 결과를 전달한다")
fun shouldPassAdultVisibilityByMember() {
val anonymousPort = FakeMainContentAllQueryPort()
createService(anonymousPort).getContents("AUDIO", null, null, null, null, null)
assertEquals(null, anonymousPort.lastMemberId)
assertFalse(anonymousPort.lastCanViewAdultContent)
val member = Member(
email = "viewer@test.com",
password = "password",
nickname = "viewer",
role = MemberRole.USER
).apply { id = 10L }
val memberPort = FakeMainContentAllQueryPort()
val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
Mockito.doReturn(true).`when`(preferenceService).canViewAdultContent(member)
createService(memberPort, preferenceService).getContents("AUDIO", null, null, null, null, member)
assertEquals(10L, memberPort.lastMemberId)
assertTrue(memberPort.lastCanViewAdultContent)
Mockito.verify(preferenceService).canViewAdultContent(member)
}
private fun createService(
port: MainContentAllQueryPort,
preferenceService: MemberContentPreferenceService = Mockito.mock(MemberContentPreferenceService::class.java),
lang: Lang = Lang.EN
): MainContentAllQueryService {
val langContext = LangContext()
langContext.setLang(lang)
return MainContentAllQueryService(
queryPort = port,
memberContentPreferenceService = preferenceService,
queryPolicy = MainContentAllQueryPolicy(),
langContext = langContext
)
}
}
private class FakeMainContentAllQueryPort : MainContentAllQueryPort {
var lastListKind: String? = null
var lastMemberId: Long? = null
var lastCanViewAdultContent: Boolean = false
var lastSort: ContentSort? = null
var lastOffset: Long? = null
var lastLimit: Int? = null
var lastOnlyFree: Boolean = false
var lastOnlyPointAvailable: Boolean = false
var lastOnlyOriginal: Boolean = false
var lastDayOfWeek: SeriesPublishedDaysOfWeek? = null
var lastLocale: String? = null
override fun countAudios(
memberId: Long?,
canViewAdultContent: Boolean,
now: LocalDateTime,
onlyFree: Boolean,
onlyPointAvailable: Boolean
): Int {
lastMemberId = memberId
lastCanViewAdultContent = canViewAdultContent
lastOnlyFree = onlyFree
lastOnlyPointAvailable = onlyPointAvailable
return 30
}
override fun findAudios(
memberId: Long?,
canViewAdultContent: Boolean,
now: LocalDateTime,
sort: ContentSort,
offset: Long,
limit: Int,
onlyFree: Boolean,
onlyPointAvailable: Boolean
): List<MainContentAllAudio> {
lastListKind = "audio"
lastMemberId = memberId
lastCanViewAdultContent = canViewAdultContent
lastSort = sort
lastOffset = offset
lastLimit = limit
lastOnlyFree = onlyFree
lastOnlyPointAvailable = onlyPointAvailable
return (1L..limit.toLong()).map { id ->
MainContentAllAudio(
audioContentId = id,
title = "audio-$id",
imageUrl = null,
price = 0,
isAdult = false,
isPointAvailable = true,
isFirstContent = id == 1L,
isOriginalSeries = false,
creatorNickname = "creator"
)
}
}
override fun countSeries(
memberId: Long?,
canViewAdultContent: Boolean,
now: LocalDateTime,
onlyOriginal: Boolean,
dayOfWeek: SeriesPublishedDaysOfWeek?
): Int {
lastMemberId = memberId
lastCanViewAdultContent = canViewAdultContent
lastOnlyOriginal = onlyOriginal
lastDayOfWeek = dayOfWeek
return 10
}
override fun findSeries(
memberId: Long?,
canViewAdultContent: Boolean,
now: LocalDateTime,
sort: ContentSort,
offset: Long,
limit: Int,
onlyOriginal: Boolean,
dayOfWeek: SeriesPublishedDaysOfWeek?,
locale: String
): List<MainContentAllSeries> {
lastListKind = "series"
lastMemberId = memberId
lastCanViewAdultContent = canViewAdultContent
lastSort = sort
lastOffset = offset
lastLimit = limit
lastOnlyOriginal = onlyOriginal
lastDayOfWeek = dayOfWeek
lastLocale = locale
return listOf(
MainContentAllSeries(
seriesId = 1L,
title = "series",
coverImageUrl = null,
creatorNickname = "creator",
isOriginal = onlyOriginal,
isAdult = false
)
)
}
}