test #432
@@ -5,9 +5,12 @@ 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.explorer.profile.creatorCommunity.comment.CreatorCommunityComment
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.CreatorCommunityLike
|
||||
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.block.BlockMember
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreference
|
||||
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
@@ -94,6 +97,145 @@ class CreatorChannelCommunityEndToEndTest @Autowired constructor(
|
||||
Mockito.verifyNoMoreInteractions(audioContentCloudFront)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("커뮤니티 상세 API는 초기 댓글 20개와 최신 답글을 반환한다")
|
||||
fun shouldReturnCommunityPostDetailWithInitialComments() {
|
||||
val fixture = createDetailFixture()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/creator-channels/community-posts/${fixture.postId}")
|
||||
.with(user(MemberAdapter(fixture.viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.postId").value(fixture.postId))
|
||||
.andExpect(jsonPath("$.data.isLiked").value(true))
|
||||
.andExpect(jsonPath("$.data.comments.comments.length()").value(20))
|
||||
.andExpect(jsonPath("$.data.comments.hasNext").value(true))
|
||||
.andExpect(jsonPath("$.data.comments.comments[0].latestReply.commentId").value(fixture.latestReplyId))
|
||||
.andExpect(jsonPath("$.data.comments.comments[0].createdAtUtc").value(org.hamcrest.Matchers.endsWith("Z")))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("커뮤니티 댓글 API는 댓글 페이지와 최신 답글을 반환한다")
|
||||
fun shouldReturnPagedCommunityCommentsWithLatestReply() {
|
||||
val fixture = createDetailFixture()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/creator-channels/community-posts/${fixture.postId}/comments")
|
||||
.param("page", "0")
|
||||
.param("size", "20")
|
||||
.with(user(MemberAdapter(fixture.viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.commentCount").value(21))
|
||||
.andExpect(jsonPath("$.data.comments.length()").value(20))
|
||||
.andExpect(jsonPath("$.data.hasNext").value(true))
|
||||
.andExpect(jsonPath("$.data.comments[0].latestReply.commentId").value(fixture.latestReplyId))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("커뮤니티 답글 API는 latestReply 없이 답글 페이지를 반환한다")
|
||||
fun shouldReturnPagedCommunityRepliesWithoutLatestReply() {
|
||||
val fixture = createDetailFixture()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/creator-channels/community-comments/${fixture.firstCommentId}/replies")
|
||||
.param("page", "0")
|
||||
.param("size", "20")
|
||||
.with(user(MemberAdapter(fixture.viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.replyCount").value(1))
|
||||
.andExpect(jsonPath("$.data.replies[0].commentId").value(fixture.latestReplyId))
|
||||
.andExpect(jsonPath("$.data.replies[0].latestReply").doesNotExist())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("댓글 불가 게시물은 빈 댓글 응답을 반환한다")
|
||||
fun shouldReturnEmptyCommentsWhenCommentUnavailable() {
|
||||
val fixture = createCommentUnavailableFixture()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/creator-channels/community-posts/${fixture.postId}/comments")
|
||||
.with(user(MemberAdapter(fixture.viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.commentCount").value(0))
|
||||
.andExpect(jsonPath("$.data.comments.length()").value(0))
|
||||
.andExpect(jsonPath("$.data.hasNext").value(false))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("성인 콘텐츠를 볼 수 없는 회원은 19금 커뮤니티 상세를 조회할 수 없다")
|
||||
fun shouldRejectAdultCommunityPostWhenViewerCannotViewAdultContent() {
|
||||
val fixture = createAdultBlockedFixture()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/creator-channels/community-posts/${fixture.postId}")
|
||||
.with(user(MemberAdapter(fixture.viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.success").value(false))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("차단 관계가 있으면 커뮤니티 상세를 조회할 수 없다")
|
||||
fun shouldRejectCommunityPostWhenViewerAndCreatorAreBlocked() {
|
||||
val fixture = createBlockedFixture()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/creator-channels/community-posts/${fixture.postId}")
|
||||
.with(user(MemberAdapter(fixture.viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.success").value(false))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("커뮤니티 댓글 API는 비밀 댓글과 차단 작성자를 필터링한다")
|
||||
fun shouldFilterSecretAndBlockedComments() {
|
||||
val fixture = createAccessPolicyFixture()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/creator-channels/community-posts/${fixture.postId}/comments")
|
||||
.with(user(MemberAdapter(fixture.viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.commentCount").value(2))
|
||||
.andExpect(jsonPath("$.data.comments.length()").value(2))
|
||||
.andExpect(jsonPath("$.data.comments[0].commentId").value(fixture.ownSecretCommentId))
|
||||
.andExpect(jsonPath("$.data.comments[1].commentId").value(fixture.publicCommentId))
|
||||
.andExpect(jsonPath("$.data.comments[1].latestReply.commentId").value(fixture.publicReplyId))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("보이지 않는 부모 댓글의 답글 조회는 오류를 반환한다")
|
||||
fun shouldRejectRepliesWhenParentCommentIsNotVisible() {
|
||||
val fixture = createAccessPolicyFixture()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/creator-channels/community-comments/${fixture.hiddenSecretCommentId}/replies")
|
||||
.with(user(MemberAdapter(fixture.viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.success").value(false))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("댓글 불가 게시물의 답글 직접 조회는 빈 응답을 반환한다")
|
||||
fun shouldReturnEmptyRepliesWhenCommentUnavailable() {
|
||||
val fixture = createCommentUnavailableFixtureWithReply()
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/creator-channels/community-comments/${fixture.commentId}/replies")
|
||||
.with(user(MemberAdapter(fixture.viewer)))
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.replyCount").value(0))
|
||||
.andExpect(jsonPath("$.data.replies.length()").value(0))
|
||||
.andExpect(jsonPath("$.data.hasNext").value(false))
|
||||
}
|
||||
|
||||
private fun createFixture(): Fixture {
|
||||
return transactionTemplate.execute {
|
||||
val now = LocalDateTime.of(2026, 6, 21, 12, 0)
|
||||
@@ -192,12 +334,13 @@ class CreatorChannelCommunityEndToEndTest @Autowired constructor(
|
||||
content: String,
|
||||
imagePath: String? = null,
|
||||
audioPath: String? = null,
|
||||
isAdult: Boolean = false
|
||||
isAdult: Boolean = false,
|
||||
isCommentAvailable: Boolean = true
|
||||
): CreatorCommunity {
|
||||
val community = CreatorCommunity(
|
||||
content = content,
|
||||
price = price,
|
||||
isCommentAvailable = true,
|
||||
isCommentAvailable = isCommentAvailable,
|
||||
isAdult = isAdult,
|
||||
audioPath = audioPath,
|
||||
imagePath = imagePath,
|
||||
@@ -210,6 +353,37 @@ class CreatorChannelCommunityEndToEndTest @Autowired constructor(
|
||||
return community
|
||||
}
|
||||
|
||||
private fun saveCommunityLike(member: Member, community: CreatorCommunity): CreatorCommunityLike {
|
||||
val like = CreatorCommunityLike(isActive = true)
|
||||
like.member = member
|
||||
like.creatorCommunity = community
|
||||
entityManager.persist(like)
|
||||
return like
|
||||
}
|
||||
|
||||
private fun saveCommunityComment(
|
||||
member: Member,
|
||||
community: CreatorCommunity,
|
||||
parent: CreatorCommunityComment? = null,
|
||||
isSecret: Boolean = false,
|
||||
isActive: Boolean = true
|
||||
): CreatorCommunityComment {
|
||||
val comment = CreatorCommunityComment(comment = "comment", isSecret = isSecret, isActive = isActive)
|
||||
comment.member = member
|
||||
comment.creatorCommunity = community
|
||||
comment.parent = parent
|
||||
entityManager.persist(comment)
|
||||
return comment
|
||||
}
|
||||
|
||||
private fun saveBlock(member: Member, blockedMember: Member): BlockMember {
|
||||
val block = BlockMember(isActive = true)
|
||||
block.member = member
|
||||
block.blockedMember = blockedMember
|
||||
entityManager.persist(block)
|
||||
return block
|
||||
}
|
||||
|
||||
private fun saveCommunityOrder(member: Member, community: CreatorCommunity): UseCan {
|
||||
val useCan = UseCan(CanUsage.PAID_COMMUNITY_POST, community.price, rewardCan = 0, isRefund = false)
|
||||
useCan.member = member
|
||||
@@ -225,6 +399,147 @@ class CreatorChannelCommunityEndToEndTest @Autowired constructor(
|
||||
.executeUpdate()
|
||||
}
|
||||
|
||||
private fun updateCommentCreatedAt(id: Long, createdAt: LocalDateTime) {
|
||||
entityManager.createQuery("update CreatorCommunityComment e set e.createdAt = :createdAt where e.id = :id")
|
||||
.setParameter("createdAt", createdAt)
|
||||
.setParameter("id", id)
|
||||
.executeUpdate()
|
||||
}
|
||||
|
||||
private fun createDetailFixture(): DetailFixture {
|
||||
return transactionTemplate.execute {
|
||||
val now = LocalDateTime.of(2026, 7, 6, 12, 0)
|
||||
val viewer = saveMember("community-detail-viewer", MemberRole.USER)
|
||||
val creator = saveMember("community-detail-creator", MemberRole.CREATOR)
|
||||
val replier = saveMember("community-detail-replier", MemberRole.USER)
|
||||
savePreference(viewer, isAdultContentVisible = true)
|
||||
val post = saveCommunity(creator, isFixed = false, price = 0, content = "detail community")
|
||||
saveCommunityLike(viewer, post)
|
||||
val comments = (1..21).map { saveCommunityComment(viewer, post) }
|
||||
val latestReply = saveCommunityComment(replier, post, parent = comments.last())
|
||||
entityManager.flush()
|
||||
comments.forEachIndexed { index, comment ->
|
||||
updateCommentCreatedAt(comment.id!!, now.plusMinutes(index.toLong()))
|
||||
}
|
||||
updateCommentCreatedAt(latestReply.id!!, now.plusMinutes(30))
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
DetailFixture(
|
||||
viewer = viewer,
|
||||
postId = post.id!!,
|
||||
firstCommentId = comments.last().id!!,
|
||||
latestReplyId = latestReply.id!!
|
||||
)
|
||||
}!!
|
||||
}
|
||||
|
||||
private fun createCommentUnavailableFixture(): PostFixture {
|
||||
return transactionTemplate.execute {
|
||||
val viewer = saveMember("community-unavailable-viewer", MemberRole.USER)
|
||||
val creator = saveMember("community-unavailable-creator", MemberRole.CREATOR)
|
||||
savePreference(viewer, isAdultContentVisible = true)
|
||||
val post = saveCommunity(
|
||||
creator = creator,
|
||||
isFixed = false,
|
||||
price = 0,
|
||||
content = "comment unavailable",
|
||||
isCommentAvailable = false
|
||||
)
|
||||
saveCommunityComment(viewer, post)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
PostFixture(viewer = viewer, postId = post.id!!)
|
||||
}!!
|
||||
}
|
||||
|
||||
private fun createAdultBlockedFixture(): PostFixture {
|
||||
return transactionTemplate.execute {
|
||||
val viewer = saveMember("community-adult-blocked-viewer", MemberRole.USER)
|
||||
val creator = saveMember("community-adult-blocked-creator", MemberRole.CREATOR)
|
||||
savePreference(viewer, isAdultContentVisible = false)
|
||||
val post = saveCommunity(
|
||||
creator = creator,
|
||||
isFixed = false,
|
||||
price = 0,
|
||||
content = "adult blocked",
|
||||
isAdult = true
|
||||
)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
PostFixture(viewer = viewer, postId = post.id!!)
|
||||
}!!
|
||||
}
|
||||
|
||||
private fun createBlockedFixture(): PostFixture {
|
||||
return transactionTemplate.execute {
|
||||
val viewer = saveMember("community-blocked-viewer", MemberRole.USER)
|
||||
val creator = saveMember("community-blocked-creator", MemberRole.CREATOR)
|
||||
savePreference(viewer, isAdultContentVisible = true)
|
||||
saveBlock(viewer, creator)
|
||||
val post = saveCommunity(creator, isFixed = false, price = 0, content = "blocked")
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
PostFixture(viewer = viewer, postId = post.id!!)
|
||||
}!!
|
||||
}
|
||||
|
||||
private fun createAccessPolicyFixture(): AccessPolicyFixture {
|
||||
return transactionTemplate.execute {
|
||||
val now = LocalDateTime.of(2026, 7, 6, 12, 0)
|
||||
val viewer = saveMember("community-policy-viewer", MemberRole.USER)
|
||||
val creator = saveMember("community-policy-creator", MemberRole.CREATOR)
|
||||
val publicWriter = saveMember("community-policy-public-writer", MemberRole.USER)
|
||||
val secretWriter = saveMember("community-policy-secret-writer", MemberRole.USER)
|
||||
val blockedWriter = saveMember("community-policy-blocked-writer", MemberRole.USER)
|
||||
val replyWriter = saveMember("community-policy-reply-writer", MemberRole.USER)
|
||||
savePreference(viewer, isAdultContentVisible = true)
|
||||
val post = saveCommunity(creator, isFixed = false, price = 0, content = "policy")
|
||||
val publicComment = saveCommunityComment(publicWriter, post)
|
||||
val ownSecretComment = saveCommunityComment(viewer, post, isSecret = true)
|
||||
val hiddenSecretComment = saveCommunityComment(secretWriter, post, isSecret = true)
|
||||
saveCommunityComment(blockedWriter, post)
|
||||
val publicReply = saveCommunityComment(replyWriter, post, parent = publicComment)
|
||||
val hiddenSecretReply = saveCommunityComment(secretWriter, post, parent = publicComment, isSecret = true)
|
||||
saveBlock(viewer, blockedWriter)
|
||||
entityManager.flush()
|
||||
updateCommentCreatedAt(publicComment.id!!, now.minusMinutes(3))
|
||||
updateCommentCreatedAt(ownSecretComment.id!!, now.minusMinutes(1))
|
||||
updateCommentCreatedAt(hiddenSecretComment.id!!, now)
|
||||
updateCommentCreatedAt(publicReply.id!!, now.plusMinutes(1))
|
||||
updateCommentCreatedAt(hiddenSecretReply.id!!, now.plusMinutes(2))
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
AccessPolicyFixture(
|
||||
viewer = viewer,
|
||||
postId = post.id!!,
|
||||
publicCommentId = publicComment.id!!,
|
||||
ownSecretCommentId = ownSecretComment.id!!,
|
||||
hiddenSecretCommentId = hiddenSecretComment.id!!,
|
||||
publicReplyId = publicReply.id!!
|
||||
)
|
||||
}!!
|
||||
}
|
||||
|
||||
private fun createCommentUnavailableFixtureWithReply(): ReplyFixture {
|
||||
return transactionTemplate.execute {
|
||||
val viewer = saveMember("community-unavailable-reply-viewer", MemberRole.USER)
|
||||
val creator = saveMember("community-unavailable-reply-creator", MemberRole.CREATOR)
|
||||
savePreference(viewer, isAdultContentVisible = true)
|
||||
val post = saveCommunity(
|
||||
creator = creator,
|
||||
isFixed = false,
|
||||
price = 0,
|
||||
content = "comment unavailable reply",
|
||||
isCommentAvailable = false
|
||||
)
|
||||
val comment = saveCommunityComment(viewer, post)
|
||||
saveCommunityComment(viewer, post, parent = comment)
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
ReplyFixture(viewer = viewer, commentId = comment.id!!)
|
||||
}!!
|
||||
}
|
||||
|
||||
private data class Fixture(
|
||||
val viewer: Member,
|
||||
val creatorId: Long,
|
||||
@@ -234,4 +549,30 @@ class CreatorChannelCommunityEndToEndTest @Autowired constructor(
|
||||
val noImagePostId: Long,
|
||||
val adultPurchasedPostId: Long
|
||||
)
|
||||
|
||||
private data class DetailFixture(
|
||||
val viewer: Member,
|
||||
val postId: Long,
|
||||
val firstCommentId: Long,
|
||||
val latestReplyId: Long
|
||||
)
|
||||
|
||||
private data class PostFixture(
|
||||
val viewer: Member,
|
||||
val postId: Long
|
||||
)
|
||||
|
||||
private data class AccessPolicyFixture(
|
||||
val viewer: Member,
|
||||
val postId: Long,
|
||||
val publicCommentId: Long,
|
||||
val ownSecretCommentId: Long,
|
||||
val hiddenSecretCommentId: Long,
|
||||
val publicReplyId: Long
|
||||
)
|
||||
|
||||
private data class ReplyFixture(
|
||||
val viewer: Member,
|
||||
val commentId: Long
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user