fix(audio-recommendation): 이벤트 상세 노출 조건으로 배너를 필터링한다
This commit is contained in:
+128
@@ -3,21 +3,32 @@ package kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.`in`.web
|
||||
import kr.co.vividnext.sodalive.admin.content.series.genre.SeriesGenre
|
||||
import kr.co.vividnext.sodalive.content.AudioContent
|
||||
import kr.co.vividnext.sodalive.content.comment.AudioContentComment
|
||||
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBanner
|
||||
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType
|
||||
import kr.co.vividnext.sodalive.content.main.tab.AudioContentMainTab
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
|
||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslation
|
||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslationPayload
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.Series
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesContent
|
||||
import kr.co.vividnext.sodalive.event.Event
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberAdapter
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.auth.Auth
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreference
|
||||
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.RecommendationSnapshot
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.CsvSource
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
|
||||
@@ -41,6 +52,95 @@ class AudioRecommendationEndToEndTest @Autowired constructor(
|
||||
private val entityManager: EntityManager,
|
||||
private val transactionTemplate: TransactionTemplate
|
||||
) {
|
||||
@ParameterizedTest
|
||||
@CsvSource(
|
||||
"ANONYMOUS, KR, ja, KO, all|false",
|
||||
"ANONYMOUS, JP, ko, JA, all|false",
|
||||
"UNVERIFIED, KR, ja, KO, all|false",
|
||||
"UNVERIFIED, JP, ko, JA, all|false",
|
||||
"VERIFIED, KR, ja, KO, all|true",
|
||||
"VERIFIED, JP, ko, JA, all|true",
|
||||
"ADMIN, KR, ja, KO, all|false|true",
|
||||
"ADMIN, JP, ko, JA, all|false|true"
|
||||
)
|
||||
@DisplayName("오디오 EVENT 응답은 요청 언어가 아닌 국가와 인증 대상 정책을 따르고 AI 필드를 추가하지 않는다")
|
||||
fun shouldApplyEventBannerDetailPolicyAndKeepAudioResponseSchema(
|
||||
viewerType: String,
|
||||
country: String,
|
||||
requestLanguage: String,
|
||||
eventLang: Lang,
|
||||
expected: String
|
||||
) {
|
||||
val fixture = transactionTemplate.execute {
|
||||
entityManager.createQuery("delete from AudioContentBanner").executeUpdate()
|
||||
if (entityManager.find(AudioContentMainTab::class.java, 2L) == null) {
|
||||
entityManager.createNativeQuery(
|
||||
"insert into audio_content_main_tab (id, title, is_active) values (2, '추천', true)"
|
||||
).executeUpdate()
|
||||
}
|
||||
val tab = entityManager.getReference(AudioContentMainTab::class.java, 2L)
|
||||
val viewer = if (viewerType == "ANONYMOUS") {
|
||||
null
|
||||
} else {
|
||||
val role = if (viewerType == "ADMIN") MemberRole.ADMIN else MemberRole.USER
|
||||
var candidate = saveMember("audio-event-viewer", role)
|
||||
while (candidate.id in setOf(16L, 17L, 17958L, 44144L, 29721L, 32050L, 37543L, 40850L)) {
|
||||
candidate = saveMember("audio-event-viewer-${candidate.id}", role)
|
||||
}
|
||||
candidate
|
||||
}
|
||||
if (viewerType == "VERIFIED") {
|
||||
entityManager.persist(
|
||||
Auth("verified", "19900101", "audio-event-ci-${viewer?.id}", "audio-event-di-${viewer?.id}", 1).apply {
|
||||
member = viewer
|
||||
}
|
||||
)
|
||||
}
|
||||
viewer?.let {
|
||||
entityManager.persist(
|
||||
MemberContentPreference(isAdultContentVisible = viewerType == "UNVERIFIED").apply {
|
||||
member = it
|
||||
}
|
||||
)
|
||||
}
|
||||
val events = mutableMapOf<String, Event>()
|
||||
listOf(Lang.KO, Lang.JA).forEach { lang ->
|
||||
listOf(null, false, true).forEachIndexed { index, audience ->
|
||||
val name = audience?.toString() ?: "all"
|
||||
events["$lang-$name"] = saveAudioEventBanner(name, lang, audience, index, tab)
|
||||
}
|
||||
}
|
||||
saveAudioEventBanner("inactive", eventLang, null, 3, tab).isActive = false
|
||||
saveAudioEventBanner("future", eventLang, null, 4, tab).startDate = LocalDateTime.of(2099, 1, 1, 0, 0)
|
||||
saveAudioEventBanner("expired", eventLang, null, 5, tab).endDate = LocalDateTime.of(2001, 1, 1, 0, 0)
|
||||
saveAudioEventBanner("wrong-event-language", eventLang, null, 6, tab).lang =
|
||||
if (eventLang == Lang.KO) Lang.JA else Lang.KO
|
||||
entityManager.persist(
|
||||
AudioContentBanner("missing-event", AudioContentBannerType.EVENT, lang = eventLang, orders = 7).apply {
|
||||
this.tab = tab
|
||||
}
|
||||
)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
viewer to events
|
||||
}!!
|
||||
|
||||
val request = get("/api/v2/audio/recommendations")
|
||||
.header("CloudFront-Viewer-Country", country)
|
||||
.header("Accept-Language", requestLanguage)
|
||||
fixture.first?.let { request.with(user(MemberAdapter(it))) }
|
||||
val response = mockMvc.perform(request)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.success").value(true))
|
||||
.andExpect(jsonPath("$.data.aiCharacters").doesNotHaveJsonPath())
|
||||
.andExpect(jsonPath("$.data.banners.length()").value(expected.split("|").size))
|
||||
expected.split("|").forEachIndexed { index, name ->
|
||||
response.andExpect(
|
||||
jsonPath("$.data.banners[$index].eventItem.id").value(fixture.second.getValue("$eventLang-$name").id)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("오디오 추천 API는 controller-service-repository를 거쳐 추천 섹션 응답을 반환한다")
|
||||
fun shouldReturnRecommendationsThroughControllerServiceAndRepository() {
|
||||
@@ -114,6 +214,34 @@ class AudioRecommendationEndToEndTest @Autowired constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveAudioEventBanner(
|
||||
name: String,
|
||||
lang: Lang,
|
||||
audience: Boolean?,
|
||||
order: Int,
|
||||
tab: AudioContentMainTab
|
||||
): Event {
|
||||
val event = Event(
|
||||
thumbnailImage = "$lang-$name.png",
|
||||
detailImage = null,
|
||||
popupImage = null,
|
||||
link = null,
|
||||
title = name,
|
||||
lang = lang,
|
||||
isAdult = audience,
|
||||
startDate = LocalDateTime.of(2000, 1, 1, 0, 0),
|
||||
endDate = LocalDateTime.of(2100, 1, 1, 0, 0)
|
||||
)
|
||||
entityManager.persist(event)
|
||||
entityManager.persist(
|
||||
AudioContentBanner("$lang-$name", AudioContentBannerType.EVENT, lang = lang, orders = order).apply {
|
||||
this.event = event
|
||||
this.tab = tab
|
||||
}
|
||||
)
|
||||
return event
|
||||
}
|
||||
|
||||
private fun createFixture(): Fixture {
|
||||
return transactionTemplate.execute {
|
||||
val now = LocalDateTime.now().minusHours(1)
|
||||
|
||||
+242
-2
@@ -17,9 +17,11 @@ import kr.co.vividnext.sodalive.content.translation.ContentTranslationPayload
|
||||
import kr.co.vividnext.sodalive.content.translation.ContentTranslationRepository
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.Series
|
||||
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesContent
|
||||
import kr.co.vividnext.sodalive.event.Event
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.auth.Auth
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMember
|
||||
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommendationScorePolicy
|
||||
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommendationVisibility
|
||||
@@ -29,6 +31,7 @@ import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.CsvSource
|
||||
import org.junit.jupiter.params.provider.MethodSource
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
@@ -49,13 +52,218 @@ class DefaultAudioRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
contentTranslationRepository: ContentTranslationRepository
|
||||
) {
|
||||
private var bannerNow = AudioContentBannerReservationFixtures.now
|
||||
private var eventNow = LocalDateTime.of(2026, 10, 31, 23, 59, 59, 123456789)
|
||||
private val repository = DefaultAudioRecommendationQueryRepository(
|
||||
queryFactory,
|
||||
entityManager,
|
||||
contentTranslationRepository,
|
||||
"https://cdn.test"
|
||||
"https://cdn.test",
|
||||
eventNowProvider = { eventNow }
|
||||
) { bannerNow }
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(
|
||||
"ANONYMOUS, KO, false, all|false",
|
||||
"ANONYMOUS, JA, false, all|false",
|
||||
"UNVERIFIED, KO, true, all|false",
|
||||
"UNVERIFIED, JA, true, all|false",
|
||||
"VERIFIED, KO, false, all|true",
|
||||
"VERIFIED, JA, false, all|true",
|
||||
"ADMIN, KO, false, all|false|true",
|
||||
"ADMIN, JA, false, all|false|true"
|
||||
)
|
||||
@DisplayName("오디오 EVENT 대상자는 배너 성인 선호와 무관하게 조회자의 인증 기록으로 판정한다")
|
||||
fun shouldApplyAudioEventVisibilityPolicyBeforeLimit(
|
||||
viewerType: String,
|
||||
lang: Lang,
|
||||
canViewAdultContent: Boolean,
|
||||
expected: String
|
||||
) {
|
||||
val viewer = saveEventViewer(viewerType)
|
||||
saveEventViewer("VERIFIED", "other-verified")
|
||||
val tab = saveContentRecommendationTab()
|
||||
listOf(null, false, true).forEachIndexed { index, audience ->
|
||||
val name = audience?.toString() ?: "all"
|
||||
val event = saveEvent(name).apply {
|
||||
this.lang = lang
|
||||
isAdult = audience
|
||||
}
|
||||
saveBanner(name, AudioContentBannerType.EVENT, index, tab = tab, lang = lang, event = event)
|
||||
}
|
||||
flushAndClear()
|
||||
|
||||
assertEquals(
|
||||
expected.split("|").map { "https://cdn.test/$it" },
|
||||
repository.findBanners(20, viewer?.id, canViewAdultContent, lang).map { it.imageUrl }
|
||||
)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(
|
||||
"ANONYMOUS, MISSING",
|
||||
"ANONYMOUS, INACTIVE",
|
||||
"ANONYMOUS, FUTURE",
|
||||
"ANONYMOUS, EXPIRED",
|
||||
"ANONYMOUS, WRONG_LANGUAGE",
|
||||
"ADMIN, MISSING",
|
||||
"ADMIN, INACTIVE",
|
||||
"ADMIN, FUTURE",
|
||||
"ADMIN, EXPIRED",
|
||||
"ADMIN, WRONG_LANGUAGE"
|
||||
)
|
||||
@DisplayName("오디오 EVENT는 관리자도 누락·비활성·기간 밖·다른 언어의 이벤트를 볼 수 없다")
|
||||
fun shouldExcludeIneligibleAudioEventsForAnonymousAndAdmin(viewerType: String, hiddenReason: String) {
|
||||
val viewer = saveEventViewer(viewerType)
|
||||
val tab = saveContentRecommendationTab()
|
||||
val event = if (hiddenReason == "MISSING") {
|
||||
null
|
||||
} else {
|
||||
saveEvent("hidden").apply {
|
||||
when (hiddenReason) {
|
||||
"INACTIVE" -> isActive = false
|
||||
"FUTURE" -> startDate = LocalDateTime.of(2099, 1, 1, 0, 0)
|
||||
"EXPIRED" -> endDate = LocalDateTime.of(2001, 1, 1, 0, 0)
|
||||
"WRONG_LANGUAGE" -> lang = Lang.JA
|
||||
}
|
||||
}
|
||||
}
|
||||
saveBanner("hidden", AudioContentBannerType.EVENT, 0, tab = tab, event = event)
|
||||
flushAndClear()
|
||||
|
||||
assertEquals(emptyList<String>(), repository.findBanners(20, viewer?.id, false).map { it.imageUrl })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("오디오 EVENT는 부적격 첫 후보를 limit 전에 제외하고 적격 두 번째 후보를 반환한다")
|
||||
fun shouldExcludeHiddenAudioEventBeforeLimit() {
|
||||
val tab = saveContentRecommendationTab()
|
||||
val hiddenEvent = saveEvent("hidden-first").apply { isAdult = true }
|
||||
val visibleEvent = saveEvent("visible-second").apply { isAdult = false }
|
||||
saveBanner("hidden-first", AudioContentBannerType.EVENT, 0, tab = tab, event = hiddenEvent)
|
||||
saveBanner("visible-second", AudioContentBannerType.EVENT, 1, tab = tab, event = visibleEvent)
|
||||
flushAndClear()
|
||||
|
||||
assertEquals(listOf("https://cdn.test/visible-second"), repository.findBanners(1, null, false).map { it.imageUrl })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("오디오 EVENT 후보가 모두 인증 대상에서 제외되면 빈 목록을 반환한다")
|
||||
fun shouldReturnEmptyWhenAllAudioEventAudiencesAreHidden() {
|
||||
val tab = saveContentRecommendationTab()
|
||||
repeat(2) { index ->
|
||||
val event = saveEvent("hidden-$index").apply { isAdult = true }
|
||||
saveBanner("hidden-$index", AudioContentBannerType.EVENT, index, tab = tab, event = event)
|
||||
}
|
||||
flushAndClear()
|
||||
|
||||
assertEquals(emptyList<String>(), repository.findBanners(20, null, false).map { it.imageUrl })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("관리자의 EVENT 대상자 우회는 오디오 배너 자체 활성·탭·언어·예약·성인 조건을 우회하지 않는다")
|
||||
fun shouldKeepBannerOwnFiltersForAdminAudioEvents() {
|
||||
val admin = saveEventViewer("ADMIN")
|
||||
val tab = saveContentRecommendationTab()
|
||||
val event = saveEvent("admin-event").apply { isAdult = true }
|
||||
listOf("inactive", "tab", "language", "future", "expired", "adult", "visible").forEachIndexed { index, name ->
|
||||
saveBanner(name, AudioContentBannerType.EVENT, index, tab = tab, event = event).apply {
|
||||
when (name) {
|
||||
"inactive" -> isActive = false
|
||||
"tab" -> this.tab = null
|
||||
"language" -> lang = Lang.JA
|
||||
"future" -> startDate = bannerNow.plusYears(1).withNano(0)
|
||||
"expired" -> endDate = bannerNow.minusYears(1).withNano(0)
|
||||
"adult" -> isAdult = true
|
||||
}
|
||||
}
|
||||
}
|
||||
flushAndClear()
|
||||
|
||||
assertEquals(listOf("https://cdn.test/visible"), repository.findBanners(20, admin?.id, false).map { it.imageUrl })
|
||||
assertEquals(
|
||||
listOf("https://cdn.test/adult", "https://cdn.test/visible"),
|
||||
repository.findBanners(20, admin?.id, true).map { it.imageUrl }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("EVENT 상세 정책은 오디오 LINK·CREATOR·SERIES 배너에 적용하지 않는다")
|
||||
fun shouldNotApplyEventPolicyToNonEventAudioBanners() {
|
||||
val tab = saveContentRecommendationTab()
|
||||
val creator = saveMember("non-event-creator", MemberRole.CREATOR)
|
||||
val series = saveSeries("non-event-series", creator, true)
|
||||
val event = saveEvent("non-event-hidden").apply {
|
||||
isActive = false
|
||||
isAdult = true
|
||||
lang = Lang.JA
|
||||
endDate = LocalDateTime.of(2001, 1, 1, 0, 0)
|
||||
}
|
||||
listOf(AudioContentBannerType.LINK, AudioContentBannerType.CREATOR, AudioContentBannerType.SERIES)
|
||||
.forEachIndexed { index, type ->
|
||||
saveBanner(type.name, type, index, creator = creator, tab = tab, event = event, series = series)
|
||||
}
|
||||
flushAndClear()
|
||||
|
||||
assertEquals(
|
||||
listOf("LINK", "CREATOR", "SERIES").map { "https://cdn.test/$it" },
|
||||
repository.findBanners(20, null, false).map { it.imageUrl }
|
||||
)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource(
|
||||
"0, START, true",
|
||||
"123456789, START, true",
|
||||
"999999999, START, true",
|
||||
"0, END, true",
|
||||
"123456789, END, true",
|
||||
"999999999, END, true",
|
||||
"0, BOTH, true",
|
||||
"123456789, BOTH, true",
|
||||
"999999999, BOTH, true",
|
||||
"0, BEFORE, false",
|
||||
"123456789, BEFORE, false",
|
||||
"999999999, BEFORE, false",
|
||||
"0, AFTER, false",
|
||||
"123456789, AFTER, false",
|
||||
"999999999, AFTER, false"
|
||||
)
|
||||
@DisplayName("오디오 EVENT는 나노초 시작·종료를 포함하고 1ns 밖을 제외하며 배너 UTC 초 clock과 독립이다")
|
||||
fun shouldApplyExactAudioEventBoundariesIndependentlyOfBannerClock(nano: Int, boundary: String, visible: Boolean) {
|
||||
eventNow = eventNow.withNano(nano)
|
||||
bannerNow = bannerNow.withNano(999999999)
|
||||
entityManager.createNativeQuery("alter table event alter column start_date timestamp(9)").executeUpdate()
|
||||
entityManager.createNativeQuery("alter table event alter column end_date timestamp(9)").executeUpdate()
|
||||
val tab = saveContentRecommendationTab()
|
||||
val start = when (boundary) {
|
||||
"START", "BOTH" -> eventNow
|
||||
"BEFORE" -> eventNow.plusNanos(1)
|
||||
else -> eventNow.minusDays(1)
|
||||
}
|
||||
val end = when (boundary) {
|
||||
"END", "BOTH" -> eventNow
|
||||
"AFTER" -> eventNow.minusNanos(1)
|
||||
else -> eventNow.plusDays(1)
|
||||
}
|
||||
val event = saveEvent("boundary").apply {
|
||||
startDate = start
|
||||
endDate = end
|
||||
}
|
||||
saveBanner("boundary", AudioContentBannerType.EVENT, 0, tab = tab, event = event).apply {
|
||||
startDate = bannerNow.withNano(0)
|
||||
endDate = bannerNow.withNano(0)
|
||||
}
|
||||
flushAndClear()
|
||||
val stored = entityManager.find(Event::class.java, event.id)
|
||||
assertEquals(start, stored.startDate)
|
||||
assertEquals(end, stored.endDate)
|
||||
|
||||
assertEquals(
|
||||
if (visible) listOf("https://cdn.test/boundary") else emptyList(),
|
||||
repository.findBanners(1, null, false).map { it.imageUrl }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("콘텐츠 추천 배너는 limit 전에 미래 및 만료 예약을 제외한다")
|
||||
fun shouldExcludeFutureAndExpiredReservationsBeforeLimit() {
|
||||
@@ -594,6 +802,34 @@ class DefaultAudioRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveEventViewer(viewerType: String, seed: String = "event-viewer"): Member? {
|
||||
if (viewerType == "ANONYMOUS") return null
|
||||
val role = if (viewerType == "ADMIN") MemberRole.ADMIN else MemberRole.USER
|
||||
val viewer = saveMember(seed, role)
|
||||
if (viewerType == "VERIFIED") {
|
||||
entityManager.persist(
|
||||
Auth("verified", "19900101", "event-ci-${viewer.id}", "event-di-${viewer.id}", 1).apply {
|
||||
member = viewer
|
||||
}
|
||||
)
|
||||
}
|
||||
return viewer
|
||||
}
|
||||
|
||||
private fun saveEvent(title: String): Event {
|
||||
val event = Event(
|
||||
thumbnailImage = "$title.png",
|
||||
detailImage = null,
|
||||
popupImage = null,
|
||||
link = null,
|
||||
title = title,
|
||||
startDate = eventNow.minusDays(1),
|
||||
endDate = eventNow.plusDays(1)
|
||||
)
|
||||
entityManager.persist(event)
|
||||
return event
|
||||
}
|
||||
|
||||
private fun saveMember(nickname: String, role: MemberRole, isActive: Boolean = true): Member {
|
||||
val member = Member(
|
||||
email = "$nickname@test.com",
|
||||
@@ -621,7 +857,9 @@ class DefaultAudioRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
creator: Member? = null,
|
||||
link: String? = null,
|
||||
tab: AudioContentMainTab? = null,
|
||||
lang: Lang = Lang.KO
|
||||
lang: Lang = Lang.KO,
|
||||
event: Event? = null,
|
||||
series: Series? = null
|
||||
): AudioContentBanner {
|
||||
val banner = AudioContentBanner(
|
||||
thumbnailImage = thumbnailImage,
|
||||
@@ -634,6 +872,8 @@ class DefaultAudioRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
banner.creator = creator
|
||||
banner.link = link
|
||||
banner.tab = tab
|
||||
banner.event = event
|
||||
banner.series = series
|
||||
entityManager.persist(banner)
|
||||
return banner
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user