test #426
@@ -27,11 +27,15 @@ import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
import kr.co.vividnext.sodalive.utils.generateFileName
|
||||
import kr.co.vividnext.sodalive.utils.validateImage
|
||||
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.transaction.support.TransactionSynchronization
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
@@ -52,6 +56,7 @@ class CreatorCommunityService(
|
||||
private val applicationEventPublisher: ApplicationEventPublisher,
|
||||
private val messageSource: SodaMessageSource,
|
||||
private val langContext: LangContext,
|
||||
private val homeFollowingNewsPublishService: HomeFollowingNewsPublishService,
|
||||
|
||||
@Value("\${cloud.aws.s3.bucket}")
|
||||
private val imageBucket: String,
|
||||
@@ -62,6 +67,8 @@ class CreatorCommunityService(
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val imageHost: String
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
@Transactional
|
||||
fun createCommunityPost(
|
||||
audioFile: MultipartFile?,
|
||||
@@ -134,6 +141,54 @@ class CreatorCommunityService(
|
||||
deepLinkId = member.id!!
|
||||
)
|
||||
)
|
||||
publishCommunityPostCreatedAfterCommit(post, member)
|
||||
}
|
||||
|
||||
private fun publishCommunityPostCreatedAfterCommit(post: CreatorCommunity, member: Member) {
|
||||
val occurredAtUtc = post.createdAt ?: LocalDateTime.now()
|
||||
val newsContent = post.newsContentPreview()
|
||||
afterCommit {
|
||||
homeFollowingNewsPublishService.publishCommunityPostCreated(
|
||||
postId = post.id!!,
|
||||
creatorId = member.id!!,
|
||||
creatorNickname = member.nickname,
|
||||
creatorProfileImagePath = member.profileImage,
|
||||
title = newsContent.take(80),
|
||||
body = newsContent,
|
||||
thumbnailImagePath = post.imagePath,
|
||||
occurredAtUtc = occurredAtUtc,
|
||||
isAdult = post.isAdult
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CreatorCommunity.newsContentPreview(): String {
|
||||
if (price <= 0) {
|
||||
return content
|
||||
}
|
||||
|
||||
val length = content.codePointCount(0, content.length)
|
||||
val previewLength = if (length > 15) 15 else length / 2
|
||||
val endIndex = content.offsetByCodePoints(0, previewLength)
|
||||
return content.substring(0, endIndex).plus("...")
|
||||
}
|
||||
|
||||
private fun afterCommit(action: () -> Unit) {
|
||||
if (!TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
runCatching(action).onFailure { ex ->
|
||||
log.warn("event=home_following_news_publish_failure error={}", ex.message, ex)
|
||||
}
|
||||
return
|
||||
}
|
||||
TransactionSynchronizationManager.registerSynchronization(
|
||||
object : TransactionSynchronization {
|
||||
override fun afterCommit() {
|
||||
runCatching(action).onFailure { ex ->
|
||||
log.warn("event=home_following_news_publish_failure error={}", ex.message, ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package kr.co.vividnext.sodalive.explorer.profile.creatorCommunity
|
||||
|
||||
import com.amazonaws.services.s3.model.ObjectMetadata
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront
|
||||
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
|
||||
import kr.co.vividnext.sodalive.can.payment.CanPaymentService
|
||||
@@ -20,6 +22,7 @@ import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
||||
@@ -32,6 +35,8 @@ import org.junit.jupiter.api.Test
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.Mockito
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.io.InputStream
|
||||
import java.time.LocalDateTime
|
||||
import java.util.Optional
|
||||
|
||||
@@ -41,7 +46,9 @@ class CreatorCommunityServiceTest {
|
||||
private lateinit var likeRepository: CreatorCommunityLikeRepository
|
||||
private lateinit var commentRepository: CreatorCommunityCommentRepository
|
||||
private lateinit var useCanRepository: UseCanRepository
|
||||
private lateinit var s3Uploader: S3Uploader
|
||||
private lateinit var applicationEventPublisher: ApplicationEventPublisher
|
||||
private lateinit var homeFollowingNewsPublishService: HomeFollowingNewsPublishService
|
||||
private lateinit var service: CreatorCommunityService
|
||||
|
||||
@BeforeEach
|
||||
@@ -51,7 +58,9 @@ class CreatorCommunityServiceTest {
|
||||
likeRepository = Mockito.mock(CreatorCommunityLikeRepository::class.java)
|
||||
commentRepository = Mockito.mock(CreatorCommunityCommentRepository::class.java)
|
||||
useCanRepository = Mockito.mock(UseCanRepository::class.java)
|
||||
s3Uploader = Mockito.mock(S3Uploader::class.java)
|
||||
applicationEventPublisher = Mockito.mock(ApplicationEventPublisher::class.java)
|
||||
homeFollowingNewsPublishService = Mockito.mock(HomeFollowingNewsPublishService::class.java)
|
||||
|
||||
service = CreatorCommunityService(
|
||||
canPaymentService = Mockito.mock(CanPaymentService::class.java),
|
||||
@@ -60,12 +69,13 @@ class CreatorCommunityServiceTest {
|
||||
likeRepository = likeRepository,
|
||||
commentRepository = commentRepository,
|
||||
useCanRepository = useCanRepository,
|
||||
s3Uploader = Mockito.mock(S3Uploader::class.java),
|
||||
objectMapper = ObjectMapper(),
|
||||
s3Uploader = s3Uploader,
|
||||
objectMapper = ObjectMapper().registerModule(KotlinModule.Builder().build()),
|
||||
audioContentCloudFront = Mockito.mock(AudioContentCloudFront::class.java),
|
||||
applicationEventPublisher = applicationEventPublisher,
|
||||
messageSource = SodaMessageSource(),
|
||||
langContext = LangContext(),
|
||||
homeFollowingNewsPublishService = homeFollowingNewsPublishService,
|
||||
imageBucket = "image-bucket",
|
||||
contentBucket = "content-bucket",
|
||||
imageHost = "https://cdn.test"
|
||||
@@ -286,6 +296,158 @@ class CreatorCommunityServiceTest {
|
||||
assertNull(post.fixedAt)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("커뮤니티 게시글 생성 성공 후 최근 소식을 게시글 정보로 발행한다")
|
||||
fun shouldPublishNewsAfterCommunityPostCreated() {
|
||||
val creator = createMember(id = 900L, role = MemberRole.CREATOR, nickname = "community-creator")
|
||||
creator.profileImage = "profile/community-creator.png"
|
||||
val createdAt = LocalDateTime.of(2026, 6, 25, 10, 0)
|
||||
Mockito.`when`(repository.save(Mockito.any(CreatorCommunity::class.java))).thenAnswer { invocation ->
|
||||
val post = invocation.getArgument<CreatorCommunity>(0)
|
||||
post.id = 901L
|
||||
post.createdAt = createdAt
|
||||
post
|
||||
}
|
||||
|
||||
service.createCommunityPost(
|
||||
audioFile = null,
|
||||
postImage = null,
|
||||
requestString = """{"content":"커뮤니티 새 게시글 본문입니다","price":0,"isCommentAvailable":true,"isAdult":true}""",
|
||||
member = creator
|
||||
)
|
||||
|
||||
Mockito.verify(homeFollowingNewsPublishService).publishCommunityPostCreated(
|
||||
postId = 901L,
|
||||
creatorId = creator.id!!,
|
||||
creatorNickname = creator.nickname!!,
|
||||
creatorProfileImagePath = creator.profileImage,
|
||||
title = "커뮤니티 새 게시글 본문입니다",
|
||||
body = "커뮤니티 새 게시글 본문입니다",
|
||||
thumbnailImagePath = null,
|
||||
occurredAtUtc = createdAt,
|
||||
isAdult = true
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("유료 커뮤니티 게시글 최근 소식은 전체 본문을 노출하지 않고 미리보기만 발행한다")
|
||||
fun shouldPublishPaidCommunityPostNewsWithMaskedContent() {
|
||||
val creator = createMember(id = 910L, role = MemberRole.CREATOR, nickname = "paid-community-creator")
|
||||
val fullContent = "유료 커뮤니티 게시글 전체 본문은 최근 소식에서 노출되면 안 됩니다"
|
||||
val createdAt = LocalDateTime.of(2026, 6, 25, 11, 0)
|
||||
Mockito.`when`(repository.save(Mockito.any(CreatorCommunity::class.java))).thenAnswer { invocation ->
|
||||
val post = invocation.getArgument<CreatorCommunity>(0)
|
||||
post.id = 911L
|
||||
post.createdAt = createdAt
|
||||
post
|
||||
}
|
||||
Mockito.`when`(
|
||||
s3Uploader.upload(
|
||||
inputStream = anyInputStream(),
|
||||
bucket = eqValue("image-bucket"),
|
||||
filePath = anyStringValue(),
|
||||
metadata = anyObjectMetadata()
|
||||
)
|
||||
).thenReturn("creator_community/911/911-image.png")
|
||||
|
||||
service.createCommunityPost(
|
||||
audioFile = null,
|
||||
postImage = paidPostImage(),
|
||||
requestString = """{"content":"$fullContent","price":10,"isCommentAvailable":true,"isAdult":false}""",
|
||||
member = creator
|
||||
)
|
||||
|
||||
Mockito.verify(homeFollowingNewsPublishService).publishCommunityPostCreated(
|
||||
postId = 911L,
|
||||
creatorId = creator.id!!,
|
||||
creatorNickname = creator.nickname!!,
|
||||
creatorProfileImagePath = creator.profileImage,
|
||||
title = "유료 커뮤니티 게시글 전체 ...",
|
||||
body = "유료 커뮤니티 게시글 전체 ...",
|
||||
thumbnailImagePath = "creator_community/911/911-image.png",
|
||||
occurredAtUtc = createdAt,
|
||||
isAdult = false
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("최근 소식 발행 실패는 커뮤니티 게시글 생성을 실패시키지 않는다")
|
||||
fun shouldNotFailCommunityPostCreationWhenNewsPublishFails() {
|
||||
val creator = createMember(id = 920L, role = MemberRole.CREATOR, nickname = "publish-failure-community")
|
||||
Mockito.`when`(repository.save(Mockito.any(CreatorCommunity::class.java))).thenAnswer { invocation ->
|
||||
val post = invocation.getArgument<CreatorCommunity>(0)
|
||||
post.id = 921L
|
||||
post.createdAt = LocalDateTime.of(2026, 6, 25, 12, 0)
|
||||
post
|
||||
}
|
||||
Mockito.doAnswer { throw IllegalStateException("publish failed") }
|
||||
.`when`(homeFollowingNewsPublishService)
|
||||
.publishCommunityPostCreated(
|
||||
postId = anyLongValue(),
|
||||
creatorId = anyLongValue(),
|
||||
creatorNickname = anyStringValue(),
|
||||
creatorProfileImagePath = Mockito.anyString(),
|
||||
title = anyStringValue(),
|
||||
body = anyStringValue(),
|
||||
thumbnailImagePath = Mockito.anyString(),
|
||||
occurredAtUtc = anyLocalDateTime(),
|
||||
isAdult = Mockito.anyBoolean()
|
||||
)
|
||||
|
||||
service.createCommunityPost(
|
||||
audioFile = null,
|
||||
postImage = null,
|
||||
requestString = """{"content":"커뮤니티 발행 실패 격리","price":0,"isCommentAvailable":true,"isAdult":false}""",
|
||||
member = creator
|
||||
)
|
||||
|
||||
Mockito.verify(repository).save(Mockito.any(CreatorCommunity::class.java))
|
||||
}
|
||||
|
||||
private fun paidPostImage(): MultipartFile {
|
||||
val pngBytes = byteArrayOf(
|
||||
0x89.toByte(),
|
||||
0x50,
|
||||
0x4E,
|
||||
0x47,
|
||||
0x0D,
|
||||
0x0A,
|
||||
0x1A,
|
||||
0x0A
|
||||
)
|
||||
return Mockito.mock(MultipartFile::class.java).also { image ->
|
||||
Mockito.`when`(image.bytes).thenReturn(pngBytes)
|
||||
Mockito.`when`(image.size).thenReturn(pngBytes.size.toLong())
|
||||
Mockito.`when`(image.contentType).thenReturn("image/png")
|
||||
Mockito.`when`(image.originalFilename).thenReturn("paid.png")
|
||||
Mockito.`when`(image.inputStream).thenReturn(pngBytes.inputStream())
|
||||
}
|
||||
}
|
||||
|
||||
private fun anyInputStream(): InputStream {
|
||||
return Mockito.any(InputStream::class.java) ?: byteArrayOf().inputStream()
|
||||
}
|
||||
|
||||
private fun anyObjectMetadata(): ObjectMetadata {
|
||||
return Mockito.any(ObjectMetadata::class.java) ?: ObjectMetadata()
|
||||
}
|
||||
|
||||
private fun anyStringValue(): String {
|
||||
return Mockito.anyString() ?: ""
|
||||
}
|
||||
|
||||
private fun anyLongValue(): Long {
|
||||
return Mockito.anyLong()
|
||||
}
|
||||
|
||||
private fun anyLocalDateTime(): LocalDateTime {
|
||||
return Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.MIN
|
||||
}
|
||||
|
||||
private fun <T> eqValue(value: T): T {
|
||||
return Mockito.eq(value) ?: value
|
||||
}
|
||||
|
||||
private fun createMember(id: Long, role: MemberRole, nickname: String): Member {
|
||||
val member = Member(
|
||||
email = "$nickname@test.com",
|
||||
|
||||
Reference in New Issue
Block a user