From c04d72b04e490fb4e790e66698c3c29b64751a08 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 22 Jun 2026 01:08:21 +0900 Subject: [PATCH] =?UTF-8?q?test(creator-channel):=20=EC=BB=A4=EB=AE=A4?= =?UTF-8?q?=EB=8B=88=ED=8B=B0=20=ED=83=AD=20E2E=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreatorChannelCommunityEndToEndTest.kt | 237 ++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt new file mode 100644 index 00000000..bf077514 --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt @@ -0,0 +1,237 @@ +package kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.`in`.web + +import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront +import kr.co.vividnext.sodalive.can.use.CanUsage +import kr.co.vividnext.sodalive.can.use.UseCan +import kr.co.vividnext.sodalive.content.ContentType +import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity +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.contentpreference.MemberContentPreference +import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito +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.boot.test.mock.mockito.MockBean +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 +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:creator-channel-live-e2e;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE" + ] +) +@AutoConfigureMockMvc +@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class]) +class CreatorChannelCommunityEndToEndTest @Autowired constructor( + private val mockMvc: MockMvc, + private val entityManager: EntityManager, + private val transactionTemplate: TransactionTemplate +) { + @MockBean + private lateinit var audioContentCloudFront: AudioContentCloudFront + + @Test + @DisplayName("커뮤니티 탭 API는 E2E로 정렬, fallback, 성인 필터, 유료 미디어 접근 정책을 반환한다") + fun shouldReturnCommunityTabThroughControllerServiceAndRepository() { + val fixture = createFixture() + Mockito.doReturn("https://signed.test/community-audio") + .`when`(audioContentCloudFront) + .generateSignedURL("community/purchased.mp3", 1000L * 60 * 30) + + mockMvc.perform( + get("/api/v2/creator-channels/${fixture.creatorId}/community") + .param("page", "-1") + .param("size", "10") + .with(user(MemberAdapter(fixture.viewer))) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.communityPostCount").value(4)) + .andExpect(jsonPath("$.data.communityPosts.length()").value(4)) + .andExpect(jsonPath("$.data.communityPosts[0].postId").value(fixture.pinnedPostId)) + .andExpect(jsonPath("$.data.communityPosts[0].isPinned").value(true)) + .andExpect(jsonPath("$.data.communityPosts[0].creatorId").value(fixture.creatorId)) + .andExpect(jsonPath("$.data.communityPosts[0].creatorNickname").value("community-e2e-creator")) + .andExpect(jsonPath("$.data.communityPosts[0].creatorProfileUrl").value("https://cdn.test/community-e2e-creator.png")) + .andExpect(jsonPath("$.data.communityPosts[0].createdAtUtc").exists()) + .andExpect(jsonPath("$.data.communityPosts[0].content").value("pinned community")) + .andExpect(jsonPath("$.data.communityPosts[0].price").value(0)) + .andExpect(jsonPath("$.data.communityPosts[0].isCommentAvailable").value(true)) + .andExpect(jsonPath("$.data.communityPosts[0].existOrdered").value(false)) + .andExpect(jsonPath("$.data.communityPosts[0].likeCount").value(0)) + .andExpect(jsonPath("$.data.communityPosts[0].commentCount").value(0)) + .andExpect(jsonPath("$.data.communityPosts[1].postId").value(fixture.purchasedPaidPostId)) + .andExpect(jsonPath("$.data.communityPosts[1].imageUrl").value("https://cdn.test/community/purchased.png")) + .andExpect(jsonPath("$.data.communityPosts[1].audioUrl").value("https://signed.test/community-audio")) + .andExpect(jsonPath("$.data.communityPosts[1].existOrdered").value(true)) + .andExpect(jsonPath("$.data.communityPosts[2].postId").value(fixture.unpurchasedPaidPostId)) + .andExpect(jsonPath("$.data.communityPosts[2].imageUrl").doesNotExist()) + .andExpect(jsonPath("$.data.communityPosts[2].audioUrl").doesNotExist()) + .andExpect(jsonPath("$.data.communityPosts[2].existOrdered").value(false)) + .andExpect(jsonPath("$.data.communityPosts[3].postId").value(fixture.noImagePostId)) + .andExpect(jsonPath("$.data.communityPosts[3].imageUrl").doesNotExist()) + .andExpect(jsonPath("$.data.communityPosts[?(@.postId == ${fixture.adultPurchasedPostId})]").isEmpty) + .andExpect(jsonPath("$.data.page").value(0)) + .andExpect(jsonPath("$.data.size").value(20)) + .andExpect(jsonPath("$.data.hasNext").value(false)) + + Mockito.verify(audioContentCloudFront) + .generateSignedURL("community/purchased.mp3", 1000L * 60 * 30) + Mockito.verifyNoMoreInteractions(audioContentCloudFront) + } + + private fun createFixture(): Fixture { + return transactionTemplate.execute { + val now = LocalDateTime.of(2026, 6, 21, 12, 0) + val viewer = saveMember("community-e2e-viewer", MemberRole.USER) + val creator = saveMember("community-e2e-creator", MemberRole.CREATOR) + savePreference(viewer, isAdultContentVisible = false) + val pinned = saveCommunity( + creator = creator, + isFixed = true, + fixedAt = now, + price = 0, + content = "pinned community", + imagePath = "community/pinned.png" + ) + val purchasedPaid = saveCommunity( + creator = creator, + isFixed = false, + price = 100, + content = "purchased paid community", + imagePath = "community/purchased.png", + audioPath = "community/purchased.mp3" + ) + val unpurchasedPaid = saveCommunity( + creator = creator, + isFixed = false, + price = 100, + content = "unpurchased paid community", + imagePath = "community/unpurchased.png", + audioPath = "community/unpurchased.mp3" + ) + val noImage = saveCommunity( + creator = creator, + isFixed = false, + price = 0, + content = "no image community" + ) + val adultPurchased = saveCommunity( + creator = creator, + isFixed = false, + price = 100, + content = "adult purchased community", + imagePath = "community/adult.png", + audioPath = "community/adult.mp3", + isAdult = true + ) + saveCommunityOrder(viewer, purchasedPaid) + saveCommunityOrder(viewer, adultPurchased) + entityManager.flush() + updateCreatedAt(pinned.id!!, now.minusHours(4)) + updateCreatedAt(purchasedPaid.id!!, now.minusHours(1)) + updateCreatedAt(unpurchasedPaid.id!!, now.minusHours(2)) + updateCreatedAt(noImage.id!!, now.minusHours(3)) + updateCreatedAt(adultPurchased.id!!, now.minusMinutes(30)) + entityManager.flush() + entityManager.clear() + + Fixture( + viewer = viewer, + creatorId = creator.id!!, + pinnedPostId = pinned.id!!, + purchasedPaidPostId = purchasedPaid.id!!, + unpurchasedPaidPostId = unpurchasedPaid.id!!, + noImagePostId = noImage.id!!, + adultPurchasedPostId = adultPurchased.id!! + ) + }!! + } + + private fun saveMember(nickname: String, role: MemberRole): Member { + val member = Member( + email = "$nickname@test.com", + password = "password", + nickname = nickname, + profileImage = "$nickname.png", + role = role + ) + entityManager.persist(member) + return member + } + + private fun savePreference(member: Member, isAdultContentVisible: Boolean): MemberContentPreference { + val preference = MemberContentPreference( + isAdultContentVisible = isAdultContentVisible, + contentType = ContentType.ALL + ) + preference.member = member + entityManager.persist(preference) + return preference + } + + private fun saveCommunity( + creator: Member, + isFixed: Boolean, + fixedAt: LocalDateTime? = null, + price: Int, + content: String, + imagePath: String? = null, + audioPath: String? = null, + isAdult: Boolean = false + ): CreatorCommunity { + val community = CreatorCommunity( + content = content, + price = price, + isCommentAvailable = true, + isAdult = isAdult, + audioPath = audioPath, + imagePath = imagePath, + isActive = true, + isFixed = isFixed, + fixedAt = fixedAt + ) + community.member = creator + entityManager.persist(community) + return community + } + + private fun saveCommunityOrder(member: Member, community: CreatorCommunity): UseCan { + val useCan = UseCan(CanUsage.PAID_COMMUNITY_POST, community.price, rewardCan = 0, isRefund = false) + useCan.member = member + useCan.communityPost = community + entityManager.persist(useCan) + return useCan + } + + private fun updateCreatedAt(id: Long, createdAt: LocalDateTime) { + entityManager.createQuery("update CreatorCommunity e set e.createdAt = :createdAt where e.id = :id") + .setParameter("createdAt", createdAt) + .setParameter("id", id) + .executeUpdate() + } + + private data class Fixture( + val viewer: Member, + val creatorId: Long, + val pinnedPostId: Long, + val purchasedPaidPostId: Long, + val unpurchasedPaidPostId: Long, + val noImagePostId: Long, + val adultPurchasedPostId: Long + ) +}