feat(home): 크리에이터 채널 홈 정책을 보강한다
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
package kr.co.vividnext.sodalive.content
|
||||
|
||||
import kr.co.vividnext.sodalive.content.pin.PinContent
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import org.springframework.transaction.PlatformTransactionManager
|
||||
import org.springframework.transaction.annotation.Propagation
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import org.springframework.transaction.support.TransactionTemplate
|
||||
import java.time.LocalDateTime
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@SpringBootTest(properties = ["cloud.aws.cloud-front.host=https://test.cloudfront.net"])
|
||||
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
|
||||
@Transactional
|
||||
class AudioContentPinConcurrencyTest @Autowired constructor(
|
||||
private val service: AudioContentService,
|
||||
private val entityManager: EntityManager,
|
||||
private val transactionManager: PlatformTransactionManager
|
||||
) {
|
||||
@Test
|
||||
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||
@DisplayName("동시 상단 고정 요청에서도 활성 고정은 9개를 넘지 않는다")
|
||||
fun shouldKeepNineActivePinsWhenPinRequestsRace() {
|
||||
val transactionTemplate = TransactionTemplate(transactionManager)
|
||||
val fixture = transactionTemplate.execute {
|
||||
val creator = saveMember("pin-race-creator", MemberRole.CREATOR)
|
||||
val existingContents = (1..8).map { saveAudioContent(creator, "existing-$it") }
|
||||
existingContents.forEach { savePinContent(creator, it) }
|
||||
val first = saveAudioContent(creator, "first-race")
|
||||
val second = saveAudioContent(creator, "second-race")
|
||||
entityManager.flush()
|
||||
PinRaceFixture(creator.id!!, first.id!!, second.id!!)
|
||||
} ?: throw IllegalStateException("fixture creation failed")
|
||||
|
||||
val start = CountDownLatch(1)
|
||||
val executor = Executors.newFixedThreadPool(2)
|
||||
try {
|
||||
val first = executor.submit {
|
||||
start.await()
|
||||
service.pinToTheTop(fixture.firstContentId, memberRef(fixture.creatorId))
|
||||
}
|
||||
val second = executor.submit {
|
||||
start.await()
|
||||
service.pinToTheTop(fixture.secondContentId, memberRef(fixture.creatorId))
|
||||
}
|
||||
|
||||
start.countDown()
|
||||
first.get(5, TimeUnit.SECONDS)
|
||||
second.get(5, TimeUnit.SECONDS)
|
||||
} finally {
|
||||
executor.shutdownNow()
|
||||
}
|
||||
|
||||
val activeContentIds = transactionTemplate.execute { activePinnedContentIds(fixture.creatorId) }.orEmpty()
|
||||
assertEquals(9, activeContentIds.size)
|
||||
assertEquals(activeContentIds.size, activeContentIds.distinct().size)
|
||||
}
|
||||
|
||||
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 saveAudioContent(creator: Member, title: String): AudioContent {
|
||||
val theme = AudioContentTheme(theme = "theme-$title", image = "$title.png", isActive = true)
|
||||
entityManager.persist(theme)
|
||||
val content = AudioContent(
|
||||
title = title,
|
||||
detail = "detail",
|
||||
languageCode = "ko",
|
||||
releaseDate = LocalDateTime.now().minusDays(1),
|
||||
isAdult = false
|
||||
)
|
||||
content.member = creator
|
||||
content.theme = theme
|
||||
content.isActive = true
|
||||
content.coverImage = "$title.png"
|
||||
content.duration = "00:10:00"
|
||||
entityManager.persist(content)
|
||||
return content
|
||||
}
|
||||
|
||||
private fun savePinContent(creator: Member, content: AudioContent): PinContent {
|
||||
val pinContent = PinContent(isActive = true)
|
||||
pinContent.member = creator
|
||||
pinContent.content = content
|
||||
entityManager.persist(pinContent)
|
||||
return pinContent
|
||||
}
|
||||
|
||||
private fun memberRef(memberId: Long): Member {
|
||||
val member = Member(email = "creator-$memberId@test.com", password = "password", nickname = "creator-$memberId")
|
||||
member.id = memberId
|
||||
return member
|
||||
}
|
||||
|
||||
private fun activePinnedContentIds(creatorId: Long): List<Long> {
|
||||
return entityManager.createQuery(
|
||||
"select p.content.id from PinContent p where p.member.id = :creatorId and p.isActive = true",
|
||||
java.lang.Long::class.java
|
||||
).setParameter("creatorId", creatorId).resultList.map { it.toLong() }
|
||||
}
|
||||
|
||||
private data class PinRaceFixture(
|
||||
val creatorId: Long,
|
||||
val firstContentId: Long,
|
||||
val secondContentId: Long
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user