test(content): 콘텐츠 전체보기 E2E 검증을 추가한다

This commit is contained in:
2026-06-27 07:32:50 +09:00
parent 0686dd6eb3
commit 55abbd2a6d
2 changed files with 214 additions and 2 deletions

View File

@@ -0,0 +1,204 @@
package kr.co.vividnext.sodalive.v2.api.content.overview.adapter.`in`.web
import kr.co.vividnext.sodalive.content.AudioContent
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
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.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.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.annotation.DirtiesContext
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.transaction.support.TransactionTemplate
import java.time.LocalDateTime
import javax.persistence.EntityManager
@SpringBootTest(
properties = [
"cloud.aws.cloud-front.host=https://cdn.test",
"spring.cache.type=none",
"spring.datasource.url=jdbc:h2:mem:content-overview-e2e;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE"
]
)
@AutoConfigureMockMvc
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class ContentOverviewEndToEndTest @Autowired constructor(
private val mockMvc: MockMvc,
private val entityManager: EntityManager,
private val transactionTemplate: TransactionTemplate
) {
@Test
@DisplayName("콘텐츠 전체보기 API는 비회원 요청을 거부한다")
fun shouldRejectAnonymousContentOverviewRequest() {
mockMvc.perform(get("/api/v2/contents"))
.andExpect(status().isUnauthorized)
}
@Test
@DisplayName("콘텐츠 전체보기 API는 인증 회원에게 New & Hot 오디오 페이지를 반환한다")
fun shouldReturnNewAndHotAudioOverviewForMember() {
val fixture = createNewAndHotFixture("content-overview-new-hot")
mockMvc.perform(
get("/api/v2/contents")
.param("type", "NEW_AND_HOT_AUDIO")
.param("page", "0")
.param("size", "20")
.with(user(MemberAdapter(fixture.viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.type").value("NEW_AND_HOT_AUDIO"))
.andExpect(jsonPath("$.data.items[0].contentId").value(fixture.audioContentId))
.andExpect(jsonPath("$.data.items[0].title").value("content-overview-new-hot-audio"))
.andExpect(jsonPath("$.data.items[0].coverImage").value("https://cdn.test/content-overview-new-hot.png"))
.andExpect(jsonPath("$.data.page").value(0))
.andExpect(jsonPath("$.data.size").value(20))
.andExpect(jsonPath("$.data.hasNext").value(false))
}
@Test
@DisplayName("콘텐츠 전체보기 API는 인증 회원에게 첫 번째 오디오 콘텐츠 페이지를 반환한다")
fun shouldReturnFirstAudioContentOverviewForMember() {
val fixture = createFirstAudioFixture("content-overview-first")
mockMvc.perform(
get("/api/v2/contents")
.param("type", "FIRST_AUDIO_CONTENT")
.param("page", "0")
.param("size", "20")
.with(user(MemberAdapter(fixture.viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.type").value("FIRST_AUDIO_CONTENT"))
.andExpect(jsonPath("$.data.items[0].contentId").value(fixture.audioContentId))
.andExpect(jsonPath("$.data.items[0].title").value("content-overview-first-audio"))
.andExpect(jsonPath("$.data.items[0].coverImage").value("https://cdn.test/content-overview-first.png"))
.andExpect(jsonPath("$.data.items[0].isFirstContent").value(true))
.andExpect(jsonPath("$.data.page").value(0))
.andExpect(jsonPath("$.data.size").value(20))
.andExpect(jsonPath("$.data.hasNext").value(false))
}
@Test
@DisplayName("콘텐츠 전체보기 API는 유효하지 않은 type을 New & Hot으로 대체한다")
fun shouldFallbackInvalidTypeToNewAndHotAudio() {
val fixture = createNewAndHotFixture("content-overview-invalid-type")
mockMvc.perform(
get("/api/v2/contents")
.param("type", "UNKNOWN")
.param("page", "0")
.param("size", "20")
.with(user(MemberAdapter(fixture.viewer)))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.type").value("NEW_AND_HOT_AUDIO"))
.andExpect(jsonPath("$.data.items[0].contentId").value(fixture.audioContentId))
.andExpect(jsonPath("$.data.page").value(0))
.andExpect(jsonPath("$.data.size").value(20))
.andExpect(jsonPath("$.data.hasNext").value(false))
}
private fun createNewAndHotFixture(prefix: String): Fixture {
return transactionTemplate.execute {
val now = LocalDateTime.now().minusHours(1)
val viewer = saveMember("$prefix-viewer", MemberRole.USER)
val creator = saveMember("$prefix-creator", MemberRole.CREATOR)
val theme = saveTheme(prefix)
val audio = saveAudio(creator, theme, "$prefix-audio", "$prefix.png", now)
saveSnapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE, audio.id!!, now)
entityManager.flush()
entityManager.clear()
Fixture(viewer = viewer, audioContentId = audio.id!!)
}!!
}
private fun createFirstAudioFixture(prefix: String): Fixture {
return transactionTemplate.execute {
val now = LocalDateTime.now().minusHours(1)
val viewer = saveMember("$prefix-viewer", MemberRole.USER)
val creator = saveMember("$prefix-creator", MemberRole.CREATOR)
val theme = saveTheme(prefix)
val audio = saveAudio(creator, theme, "$prefix-audio", "$prefix.png", now)
entityManager.flush()
entityManager.clear()
Fixture(viewer = viewer, audioContentId = audio.id!!)
}!!
}
private fun saveMember(nickname: String, role: MemberRole): Member {
val member = Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname,
role = role
)
entityManager.persist(member)
return member
}
private fun saveTheme(prefix: String): AudioContentTheme {
val theme = AudioContentTheme(theme = "$prefix-theme", image = "$prefix-theme.png", isActive = true)
entityManager.persist(theme)
return theme
}
private fun saveAudio(
creator: Member,
theme: AudioContentTheme,
title: String,
coverImage: String,
releaseDate: LocalDateTime
): AudioContent {
val audio = AudioContent(
title = title,
detail = "detail",
languageCode = "ko",
releaseDate = releaseDate,
isAdult = false,
price = 100,
isPointAvailable = true
)
audio.member = creator
audio.theme = theme
audio.isActive = true
audio.coverImage = coverImage
audio.duration = "00:10"
entityManager.persist(audio)
return audio
}
private fun saveSnapshot(sectionType: RecommendedSectionType, targetId: Long, snapshotAt: LocalDateTime) {
entityManager.persist(
RecommendationSnapshot(
sectionType = sectionType,
targetId = targetId,
score = 1.0,
snapshotAt = snapshotAt,
randomTieBreaker = 0.0
)
)
}
private data class Fixture(
val viewer: Member,
val audioContentId: Long
)
}