feat(content): 오디오 업로드 최근 소식을 발행한다

This commit is contained in:
2026-06-26 02:49:57 +09:00
parent 36a60c76eb
commit 9fc6643c18
2 changed files with 249 additions and 1 deletions
@@ -21,6 +21,7 @@ import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.i18n.translation.ResourceTranslationJobScheduler
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
import kr.co.vividnext.sodalive.v2.recommendation.application.CreatorContentViewHistoryService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
@@ -57,6 +58,7 @@ class AudioContentServiceTest {
private lateinit var audioContentCloudFront: AudioContentCloudFront
private lateinit var applicationEventPublisher: ApplicationEventPublisher
private lateinit var contentThemeTranslationRepository: ContentThemeTranslationRepository
private lateinit var homeFollowingNewsPublishService: HomeFollowingNewsPublishService
private lateinit var service: AudioContentService
@@ -80,6 +82,7 @@ class AudioContentServiceTest {
audioContentCloudFront = Mockito.mock(AudioContentCloudFront::class.java)
applicationEventPublisher = Mockito.mock(ApplicationEventPublisher::class.java)
contentThemeTranslationRepository = Mockito.mock(ContentThemeTranslationRepository::class.java)
homeFollowingNewsPublishService = Mockito.mock(HomeFollowingNewsPublishService::class.java)
service = AudioContentService(
repository = repository,
@@ -103,6 +106,7 @@ class AudioContentServiceTest {
messageSource = SodaMessageSource(),
langContext = LangContext(),
contentThemeTranslationRepository = contentThemeTranslationRepository,
homeFollowingNewsPublishService = homeFollowingNewsPublishService,
audioContentBucket = "audio-bucket",
coverImageBucket = "cover-bucket",
coverImageHost = "https://cdn.test"
@@ -273,6 +277,178 @@ class AudioContentServiceTest {
assertTrue(output.out.contains("contentId=${audioContent.id}"))
}
@Test
@DisplayName("업로드 완료 시 즉시 공개 콘텐츠는 최근 소식을 발행한다")
fun shouldPublishNewsWhenUploadCompleteMakesContentPublicImmediately() {
val creator = createMember(id = 2100L, nickname = "audio-creator")
creator.profileImage = "profile/audio-creator.png"
val audioContent = createAudioContent(creator = creator, isAdult = true)
audioContent.isActive = false
audioContent.releaseDate = LocalDateTime.of(2026, 6, 25, 9, 0)
audioContent.createdAt = LocalDateTime.of(2026, 6, 25, 9, 0)
Mockito.`when`(repository.findById(audioContent.id!!)).thenReturn(Optional.of(audioContent))
service.uploadComplete(
contentId = audioContent.id!!,
content = "output/${audioContent.id}/${audioContent.id}-content.mp3",
duration = "00:03:00"
)
Mockito.verify(homeFollowingNewsPublishService).publishContentUploaded(
contentId = audioContent.id!!,
creatorId = creator.id!!,
creatorNickname = creator.nickname!!,
creatorProfileImagePath = creator.profileImage,
title = audioContent.title,
body = audioContent.detail,
thumbnailImagePath = audioContent.coverImage,
occurredAtUtc = audioContent.createdAt!!,
visibleFromAtUtc = audioContent.createdAt!!,
isAdult = true
)
}
@Test
@DisplayName("유료 오디오가 상세 비공개이면 최근 소식은 전체 상세를 노출하지 않는다")
fun shouldMaskPaidAudioDetailWhenPublishingNews() {
val creator = createMember(id = 2120L, nickname = "paid-audio-creator")
val audioContent = createAudioContent(creator = creator)
audioContent.isActive = false
audioContent.isFullDetailVisible = false
audioContent.detail = "유료 오디오 상세 설명 전체 본문은 최근 소식에서 모두 보이면 안 됩니다"
audioContent.releaseDate = LocalDateTime.of(2026, 6, 25, 9, 0)
audioContent.createdAt = LocalDateTime.of(2026, 6, 25, 9, 0)
Mockito.`when`(repository.findById(audioContent.id!!)).thenReturn(Optional.of(audioContent))
service.uploadComplete(
contentId = audioContent.id!!,
content = "output/${audioContent.id}/${audioContent.id}-content.mp3",
duration = "00:03:00"
)
Mockito.verify(homeFollowingNewsPublishService).publishContentUploaded(
contentId = audioContent.id!!,
creatorId = creator.id!!,
creatorNickname = creator.nickname!!,
creatorProfileImagePath = creator.profileImage,
title = audioContent.title,
body = "유료 오디오 상세 설명 전체 본문은 ...",
thumbnailImagePath = audioContent.coverImage,
occurredAtUtc = audioContent.createdAt!!,
visibleFromAtUtc = audioContent.createdAt!!,
isAdult = false
)
}
@Test
@DisplayName("최근 소식 발행 실패는 업로드 완료 처리를 실패시키지 않는다")
fun shouldNotFailUploadCompleteWhenNewsPublishFails() {
val creator = createMember(id = 2130L, nickname = "publish-failure-creator")
val audioContent = createAudioContent(creator = creator)
audioContent.isActive = false
audioContent.releaseDate = LocalDateTime.of(2026, 6, 25, 9, 0)
audioContent.createdAt = LocalDateTime.of(2026, 6, 25, 9, 0)
Mockito.`when`(repository.findById(audioContent.id!!)).thenReturn(Optional.of(audioContent))
Mockito.doAnswer { throw IllegalStateException("publish failed") }
.`when`(homeFollowingNewsPublishService)
.publishContentUploaded(
contentId = anyLongValue(),
creatorId = anyLongValue(),
creatorNickname = anyStringValue(),
creatorProfileImagePath = Mockito.anyString(),
title = anyStringValue(),
body = anyStringValue(),
thumbnailImagePath = Mockito.anyString(),
occurredAtUtc = anyLocalDateTime(),
visibleFromAtUtc = anyLocalDateTime(),
isAdult = Mockito.anyBoolean()
)
service.uploadComplete(
contentId = audioContent.id!!,
content = "output/${audioContent.id}/${audioContent.id}-content.mp3",
duration = "00:03:00"
)
assertTrue(audioContent.isActive)
}
@Test
@DisplayName("업로드 완료 시 공개 시각이 생성 이후 업로드 전이면 최근 소식을 발행한다")
fun shouldPublishNewsWhenReleaseDatePassedBeforeUploadComplete() {
val creator = createMember(id = 2150L, nickname = "audio-late-upload-creator")
val audioContent = createAudioContent(creator = creator)
audioContent.isActive = false
audioContent.createdAt = LocalDateTime.now().minusHours(2)
audioContent.releaseDate = LocalDateTime.now().minusHours(1)
Mockito.`when`(repository.findById(audioContent.id!!)).thenReturn(Optional.of(audioContent))
service.uploadComplete(
contentId = audioContent.id!!,
content = "output/${audioContent.id}/${audioContent.id}-content.mp3",
duration = "00:03:00"
)
Mockito.verify(homeFollowingNewsPublishService).publishContentUploaded(
contentId = audioContent.id!!,
creatorId = creator.id!!,
creatorNickname = creator.nickname!!,
creatorProfileImagePath = creator.profileImage,
title = audioContent.title,
body = audioContent.detail,
thumbnailImagePath = audioContent.coverImage,
occurredAtUtc = audioContent.createdAt!!,
visibleFromAtUtc = audioContent.releaseDate!!,
isAdult = false
)
}
@Test
@DisplayName("업로드 완료 시 예약 공개 콘텐츠는 생성 시점에 최근 소식을 발행하지 않는다")
fun shouldNotPublishNewsWhenUploadCompleteKeepsScheduledContentInactive() {
val creator = createMember(id = 2200L, nickname = "scheduled-creator")
val audioContent = createAudioContent(creator = creator)
audioContent.isActive = false
audioContent.createdAt = LocalDateTime.of(2026, 6, 25, 9, 0)
audioContent.releaseDate = LocalDateTime.of(2026, 6, 26, 9, 0)
Mockito.`when`(repository.findById(audioContent.id!!)).thenReturn(Optional.of(audioContent))
service.uploadComplete(
contentId = audioContent.id!!,
content = "output/${audioContent.id}/${audioContent.id}-content.mp3",
duration = "00:03:00"
)
Mockito.verifyNoInteractions(homeFollowingNewsPublishService)
}
@Test
@DisplayName("예약 콘텐츠 공개 작업은 활성화 시점에 최근 소식을 발행한다")
fun shouldPublishNewsWhenReleaseContentActivatesScheduledContent() {
val creator = createMember(id = 2300L, nickname = "release-creator")
creator.profileImage = "profile/release-creator.png"
val audioContent = createAudioContent(creator = creator)
audioContent.isActive = false
audioContent.createdAt = LocalDateTime.of(2026, 6, 24, 9, 0)
audioContent.releaseDate = LocalDateTime.of(2026, 6, 25, 9, 0)
Mockito.`when`(repository.getNotReleaseContent()).thenReturn(listOf(audioContent))
service.releaseContent()
Mockito.verify(homeFollowingNewsPublishService).publishContentUploaded(
contentId = audioContent.id!!,
creatorId = creator.id!!,
creatorNickname = creator.nickname!!,
creatorProfileImagePath = creator.profileImage,
title = audioContent.title,
body = audioContent.detail,
thumbnailImagePath = audioContent.coverImage,
occurredAtUtc = audioContent.createdAt!!,
visibleFromAtUtc = audioContent.releaseDate!!,
isAdult = false
)
}
private fun createMember(id: Long, nickname: String): Member {
val member = Member(
email = "$nickname@test.com",
@@ -283,6 +459,14 @@ class AudioContentServiceTest {
return member
}
private fun anyLongValue(): Long {
return Mockito.anyLong()
}
private fun anyStringValue(): String {
return Mockito.anyString() ?: ""
}
private fun createAudioContent(creator: Member, isAdult: Boolean = false): AudioContent {
val theme = AudioContentTheme(theme = "수면", image = "sleep.png")
theme.id = 300L