fix(live-room): 제한 사용자의 입장과 토큰 발급을 차단한다

This commit is contained in:
2026-09-17 19:15:58 +09:00
parent 7c5d3874db
commit a8b16dca5a
2 changed files with 449 additions and 0 deletions
@@ -812,6 +812,14 @@ class LiveRoomService(
)
}
if (
member.role == MemberRole.CREATOR &&
room.member!!.id!! != member.id!! &&
!room.isAvailableJoinCreator
) {
throw SodaException(messageKey = "live.room.not_found")
}
if (room.member!!.id!! != member.id!! && !member.canEnter(room.genderRestriction)) {
throw SodaException(messageKey = "live.room.gender_restricted")
}
@@ -975,6 +983,18 @@ class LiveRoomService(
throw SodaException(messageKey = "live.room.info_not_found")
}
if (
member.role == MemberRole.CREATOR &&
room.member!!.id!! != member.id!! &&
!room.isAvailableJoinCreator
) {
throw SodaException(messageKey = "live.room.not_found")
}
if (room.member!!.id!! != member.id!! && !member.canEnter(room.genderRestriction)) {
throw SodaException(messageKey = "live.room.gender_restricted")
}
val currentTimeStamp = Date().time
val expireTimestamp = (currentTimeStamp + (60 * 60 * 24 * 1000)) / 1000
@@ -0,0 +1,429 @@
package kr.co.vividnext.sodalive.live.room
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.agora.RtcTokenBuilder
import kr.co.vividnext.sodalive.agora.RtmTokenBuilder
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.can.CanRepository
import kr.co.vividnext.sodalive.can.charge.ChargeRepository
import kr.co.vividnext.sodalive.can.payment.CanPaymentService
import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
import kr.co.vividnext.sodalive.explorer.profile.CreatorDonationRankingService
import kr.co.vividnext.sodalive.fcm.PushTokenRepository
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
import kr.co.vividnext.sodalive.live.room.cancel.LiveRoomCancelRepository
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfo
import kr.co.vividnext.sodalive.live.room.info.LiveRoomInfoRedisRepository
import kr.co.vividnext.sodalive.live.room.kickout.LiveRoomKickOutService
import kr.co.vividnext.sodalive.live.room.menu.LiveRoomMenuService
import kr.co.vividnext.sodalive.live.room.visit.LiveRoomVisitService
import kr.co.vividnext.sodalive.live.roulette.NewRouletteRepository
import kr.co.vividnext.sodalive.live.signature.SignatureCanRepository
import kr.co.vividnext.sodalive.live.tag.LiveTagRepository
import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.auth.Auth
import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.mockito.Mockito
import java.time.LocalDateTime
import java.util.Optional
class LiveRoomServiceTest {
private lateinit var repository: LiveRoomRepository
private lateinit var roomInfoRepository: LiveRoomInfoRedisRepository
private lateinit var canPaymentService: CanPaymentService
private lateinit var memberRepository: MemberRepository
private lateinit var blockMemberRepository: BlockMemberRepository
private lateinit var explorerQueryRepository: ExplorerQueryRepository
private lateinit var rouletteRepository: NewRouletteRepository
private lateinit var pushTokenRepository: PushTokenRepository
private lateinit var rtcTokenBuilder: RtcTokenBuilder
private lateinit var rtmTokenBuilder: RtmTokenBuilder
private lateinit var service: LiveRoomService
@BeforeEach
fun setup() {
repository = mock()
roomInfoRepository = mock()
canPaymentService = mock()
memberRepository = mock()
blockMemberRepository = mock()
explorerQueryRepository = mock()
rouletteRepository = mock()
pushTokenRepository = mock()
rtcTokenBuilder = mock()
rtmTokenBuilder = mock()
service = LiveRoomService(
menuService = mock<LiveRoomMenuService>(),
messageSource = mock<SodaMessageSource>(),
langContext = LangContext(),
repository = repository,
rouletteRepository = rouletteRepository,
roomInfoRepository = roomInfoRepository,
roomCancelRepository = mock<LiveRoomCancelRepository>(),
kickOutService = mock<LiveRoomKickOutService>(),
blockMemberRepository = blockMemberRepository,
signatureCanRepository = mock<SignatureCanRepository>(),
applicationEventPublisher = mock(),
useCanCalculateRepository = mock<UseCanCalculateRepository>(),
reservationRepository = mock<LiveReservationRepository>(),
explorerQueryRepository = explorerQueryRepository,
creatorDonationRankingService = mock<CreatorDonationRankingService>(),
roomVisitService = mock<LiveRoomVisitService>(),
canPaymentService = canPaymentService,
chargeRepository = mock<ChargeRepository>(),
pushTokenRepository = pushTokenRepository,
memberRepository = memberRepository,
tagRepository = mock<LiveTagRepository>(),
memberContentPreferenceService = mock<MemberContentPreferenceService>(),
canRepository = mock<CanRepository>(),
objectMapper = ObjectMapper(),
s3Uploader = mock<S3Uploader>(),
rtcTokenBuilder = rtcTokenBuilder,
rtmTokenBuilder = rtmTokenBuilder,
agoraAppId = "test-agora-app-id",
agoraAppCertificate = "test-agora-app-certificate",
coverImageBucket = "test-cover-image-bucket",
cloudFrontHost = "https://test-cloudfront-host"
)
}
@Test
@DisplayName("다른 크리에이터는 크리에이터 입장 제한 방에 입장할 수 없다")
fun shouldRejectDifferentCreatorBeforePaymentOrRoomInfoMutation() {
val owner = createMember(id = 1L, role = MemberRole.CREATOR)
val enteringCreator = createMember(id = 2L, role = MemberRole.CREATOR).also { it.pgChargeCan = 10 }
val room = createRoom(owner = owner, price = 10)
prepareAllowedEntry(room)
Mockito.`when`(memberRepository.findById(enteringCreator.id!!)).thenReturn(Optional.of(enteringCreator))
val exception = assertThrows<SodaException> {
service.enterLive(EnterOrQuitLiveRoomRequest(roomId = room.id!!, container = "web"), enteringCreator)
}
assertEquals("live.room.not_found", exception.messageKey)
Mockito.verifyNoInteractions(roomInfoRepository, canPaymentService)
}
@Test
@DisplayName("성별 제한에 맞지 않는 일반 사용자는 입장할 수 없다")
fun shouldRejectGenderMismatchedUserBeforePaymentOrRoomInfoMutation() {
val owner = createMember(id = 1L, role = MemberRole.CREATOR)
val member = createMember(id = 2L, gender = Gender.FEMALE)
val room = createRoom(owner = owner, price = 10, genderRestriction = GenderRestriction.MALE_ONLY)
Mockito.`when`(repository.getLiveRoom(room.id!!)).thenReturn(room)
val exception = assertThrows<SodaException> {
service.enterLive(EnterOrQuitLiveRoomRequest(roomId = room.id!!, container = "web"), member)
}
assertEquals("live.room.gender_restricted", exception.messageKey)
Mockito.verifyNoInteractions(roomInfoRepository, canPaymentService)
}
@Test
@DisplayName("방 생성자는 크리에이터 및 성별 제한과 무관하게 입장할 수 있다")
fun shouldAllowRoomOwner() {
val owner = createMember(id = 1L, role = MemberRole.CREATOR, gender = Gender.MALE)
val room = createRoom(owner = owner, genderRestriction = GenderRestriction.FEMALE_ONLY)
val roomInfo = prepareAllowedEntry(room)
service.enterLive(EnterOrQuitLiveRoomRequest(roomId = room.id!!, container = "web"), owner)
assertEquals(1, roomInfo.speakerCount)
Mockito.verify(roomInfoRepository).save(roomInfo)
}
@Test
@DisplayName("일반 사용자는 조건에 맞으면 크리에이터 입장 제한 방에 입장할 수 있다")
fun shouldAllowEligibleUser() {
val owner = createMember(id = 1L, role = MemberRole.CREATOR)
val member = createMember(id = 2L, gender = Gender.MALE)
val room = createRoom(owner = owner, genderRestriction = GenderRestriction.MALE_ONLY)
val roomInfo = prepareAllowedEntry(room)
service.enterLive(EnterOrQuitLiveRoomRequest(roomId = room.id!!, container = "web"), member)
assertEquals(1, roomInfo.listenerCount)
Mockito.verify(roomInfoRepository).save(roomInfo)
}
@Test
@DisplayName("성별 미설정 사용자는 기존 성별 제한 정책대로 입장할 수 있다")
fun shouldAllowUserWithNoGender() {
val owner = createMember(id = 1L, role = MemberRole.CREATOR)
val member = createMember(id = 2L, gender = Gender.NONE)
val room = createRoom(owner = owner, genderRestriction = GenderRestriction.MALE_ONLY)
val roomInfo = prepareAllowedEntry(room)
service.enterLive(EnterOrQuitLiveRoomRequest(roomId = room.id!!, container = "web"), member)
assertEquals(1, roomInfo.listenerCount)
Mockito.verify(roomInfoRepository).save(roomInfo)
}
@Test
@DisplayName("다른 크리에이터는 제한 방 정보 조회로 토큰을 발급받을 수 없다")
fun shouldRejectDifferentCreatorFromRoomInfoBeforeTokenGeneration() {
val owner = createMember(id = 1L, role = MemberRole.CREATOR)
val viewer = createMember(id = 2L, role = MemberRole.CREATOR)
val room = createRoom(owner = owner)
prepareSuccessfulRoomInfo(room)
val exception = assertThrows<SodaException> {
service.getRoomInfo(roomId = room.id!!, member = viewer)
}
assertEquals("live.room.not_found", exception.messageKey)
Mockito.verifyNoInteractions(rtcTokenBuilder, rtmTokenBuilder)
}
@Test
@DisplayName("성별 제한에 맞지 않는 사용자는 방 정보 조회로 토큰을 발급받을 수 없다")
fun shouldRejectGenderMismatchedNonOwnerFromRoomInfoBeforeTokenGeneration() {
val owner = createMember(id = 1L, role = MemberRole.CREATOR)
val viewer = createMember(id = 2L, gender = Gender.FEMALE)
val room = createRoom(owner = owner, genderRestriction = GenderRestriction.MALE_ONLY)
prepareSuccessfulRoomInfo(room)
val exception = assertThrows<SodaException> {
service.getRoomInfo(roomId = room.id!!, member = viewer)
}
assertEquals("live.room.gender_restricted", exception.messageKey)
Mockito.verifyNoInteractions(rtcTokenBuilder, rtmTokenBuilder)
}
@Test
@DisplayName("크리에이터와 성별 제한이 함께 적용되면 크리에이터 제한을 먼저 반환한다")
fun shouldPrioritizeCreatorRestrictionOverGenderRestrictionInRoomInfo() {
val owner = createMember(id = 1L, role = MemberRole.CREATOR)
val viewer = createMember(id = 2L, role = MemberRole.CREATOR, gender = Gender.FEMALE)
val room = createRoom(owner = owner, genderRestriction = GenderRestriction.MALE_ONLY)
prepareSuccessfulRoomInfo(room)
val exception = assertThrows<SodaException> {
service.getRoomInfo(roomId = room.id!!, member = viewer)
}
assertEquals("live.room.not_found", exception.messageKey)
Mockito.verifyNoInteractions(rtcTokenBuilder, rtmTokenBuilder)
}
@Test
@DisplayName("입장 거절 뒤 같은 제한 방 정보 조회도 토큰을 발급하지 않는다")
fun shouldNotGenerateTokensAfterEnterLiveRejectsDifferentCreator() {
val owner = createMember(id = 1L, role = MemberRole.CREATOR)
val viewer = createMember(id = 2L, role = MemberRole.CREATOR)
val room = createRoom(owner = owner)
prepareSuccessfulRoomInfo(room)
Mockito.`when`(repository.getLiveRoom(room.id!!)).thenReturn(room)
val enterException = assertThrows<SodaException> {
service.enterLive(EnterOrQuitLiveRoomRequest(roomId = room.id!!, container = "web"), viewer)
}
val roomInfoException = assertThrows<SodaException> {
service.getRoomInfo(roomId = room.id!!, member = viewer)
}
assertEquals("live.room.not_found", enterException.messageKey)
assertEquals("live.room.not_found", roomInfoException.messageKey)
Mockito.verifyNoInteractions(rtcTokenBuilder, rtmTokenBuilder)
}
@Test
@DisplayName("방 생성자는 모든 크리에이터와 성별 제한이 있어도 방 정보 토큰을 발급받는다")
fun shouldAllowRoomOwnerToGetRoomInfoTokens() {
val owner = createMember(id = 1L, role = MemberRole.CREATOR, gender = Gender.MALE)
val room = createRoom(owner = owner, genderRestriction = GenderRestriction.FEMALE_ONLY)
prepareSuccessfulRoomInfo(room)
assertTokenizedRoomInfo(room, owner)
}
@Test
@DisplayName("성별이 맞는 일반 사용자는 제한 방 정보 토큰을 발급받는다")
fun shouldAllowGenderMatchedUserToGetRoomInfoTokens() {
val owner = createMember(id = 1L, role = MemberRole.CREATOR)
val viewer = createMember(id = 2L, gender = Gender.MALE)
val room = createRoom(owner = owner, genderRestriction = GenderRestriction.MALE_ONLY)
prepareSuccessfulRoomInfo(room)
assertTokenizedRoomInfo(room, viewer)
}
@Test
@DisplayName("크리에이터 입장이 허용된 다른 크리에이터는 방 정보 토큰을 발급받는다")
fun shouldAllowDifferentCreatorWhenCreatorJoiningIsEnabledToGetRoomInfoTokens() {
val owner = createMember(id = 1L, role = MemberRole.CREATOR)
val viewer = createMember(id = 2L, role = MemberRole.CREATOR, gender = Gender.MALE)
val room = createRoom(
owner = owner,
isAvailableJoinCreator = true,
genderRestriction = GenderRestriction.MALE_ONLY
)
prepareSuccessfulRoomInfo(room)
assertTokenizedRoomInfo(room, viewer)
}
@Test
@DisplayName("인증 성별은 프로필 성별보다 우선하여 방 정보 제한을 판정한다")
fun shouldUseAuthGenderBeforeProfileGenderForRoomInfoTokens() {
val owner = createMember(id = 1L, role = MemberRole.CREATOR)
val viewer = createMember(id = 2L, gender = Gender.MALE)
viewer.auth = Auth(
name = "viewer",
birth = "20000101",
uniqueCi = "unique-ci",
di = "di",
gender = 2
)
val room = createRoom(owner = owner, genderRestriction = GenderRestriction.FEMALE_ONLY)
prepareSuccessfulRoomInfo(room)
assertTokenizedRoomInfo(room, viewer)
}
@Test
@DisplayName("성별 미설정 사용자는 기존 정책대로 방 정보 토큰을 발급받는다")
fun shouldAllowUserWithNoGenderToGetRoomInfoTokens() {
val owner = createMember(id = 1L, role = MemberRole.CREATOR)
val viewer = createMember(id = 2L, gender = Gender.NONE)
val room = createRoom(owner = owner, genderRestriction = GenderRestriction.MALE_ONLY)
prepareSuccessfulRoomInfo(room)
assertTokenizedRoomInfo(room, viewer)
}
@Test
@DisplayName("상호 차단된 사용자는 기존 방 정보 예외로 토큰을 발급받을 수 없다")
fun shouldRejectMutuallyBlockedUserFromRoomInfoBeforeTokenGeneration() {
val owner = createMember(id = 1L, role = MemberRole.CREATOR)
val viewer = createMember(id = 2L, gender = Gender.MALE)
val room = createRoom(owner = owner, genderRestriction = GenderRestriction.MALE_ONLY)
prepareSuccessfulRoomInfo(room)
Mockito.`when`(blockMemberRepository.isBlocked(Mockito.anyLong(), Mockito.anyLong())).thenReturn(true)
val exception = assertThrows<SodaException> {
service.getRoomInfo(roomId = room.id!!, member = viewer)
}
assertEquals("live.room.info_not_found", exception.messageKey)
Mockito.verifyNoInteractions(rtcTokenBuilder, rtmTokenBuilder)
}
private fun prepareAllowedEntry(room: LiveRoom): LiveRoomInfo {
val roomInfo = LiveRoomInfo(roomId = room.id!!)
Mockito.`when`(repository.getLiveRoom(room.id!!)).thenReturn(room)
Mockito.`when`(roomInfoRepository.findById(room.id!!)).thenReturn(Optional.of(roomInfo))
Mockito.`when`(roomInfoRepository.save(roomInfo)).thenReturn(roomInfo)
return roomInfo
}
private fun prepareSuccessfulRoomInfo(room: LiveRoom): LiveRoomInfo {
val roomInfo = LiveRoomInfo(roomId = room.id!!)
room.member!!.isVisibleDonationRank = false
room.channelName = "SODA_LIVE_CHANNEL_TEST"
Mockito.`when`(roomInfoRepository.findById(room.id!!)).thenReturn(Optional.of(roomInfo))
Mockito.`when`(repository.findById(room.id!!)).thenReturn(Optional.of(room))
Mockito.`when`(blockMemberRepository.isBlocked(Mockito.anyLong(), Mockito.anyLong())).thenReturn(false)
Mockito.`when`(
rtcTokenBuilder.buildTokenWithUid(
Mockito.anyString(),
Mockito.anyString(),
Mockito.anyString(),
Mockito.anyString(),
Mockito.anyInt()
)
).thenReturn("rtc-token")
Mockito.`when`(
rtmTokenBuilder.buildToken(
Mockito.anyString(),
Mockito.anyString(),
Mockito.anyString(),
Mockito.anyInt()
)
).thenReturn("rtm-token")
Mockito.`when`(explorerQueryRepository.getNotificationUserIds(room.member!!.id!!)).thenReturn(emptyList())
Mockito.`when`(rouletteRepository.findByCreatorId(room.member!!.id!!)).thenReturn(emptyList())
Mockito.`when`(pushTokenRepository.findByMemberId(room.member!!.id!!)).thenReturn(emptyList())
return roomInfo
}
private fun assertTokenizedRoomInfo(room: LiveRoom, member: Member) {
val response = service.getRoomInfo(roomId = room.id!!, member = member)
assertEquals(room.channelName, response.channelName)
assertEquals("rtc-token", response.rtcToken)
assertEquals("rtm-token", response.rtmToken)
assertEquals("rtc-token", response.v2vWorkerToken)
Mockito.verify(rtcTokenBuilder, Mockito.times(2)).buildTokenWithUid(
Mockito.anyString(),
Mockito.anyString(),
Mockito.anyString(),
Mockito.anyString(),
Mockito.anyInt()
)
Mockito.verify(rtmTokenBuilder).buildToken(
Mockito.anyString(),
Mockito.anyString(),
Mockito.anyString(),
Mockito.anyInt()
)
}
private fun createRoom(
owner: Member,
price: Int = 0,
isAvailableJoinCreator: Boolean = false,
genderRestriction: GenderRestriction = GenderRestriction.ALL
): LiveRoom {
return LiveRoom(
title = "test room",
notice = "test notice",
beginDateTime = LocalDateTime.now(),
numberOfPeople = 10,
coverImage = "cover/image.png",
isAdult = false,
price = price,
isAvailableJoinCreator = isAvailableJoinCreator,
genderRestriction = genderRestriction
).also {
it.id = 100L
it.member = owner
}
}
private fun createMember(
id: Long,
role: MemberRole = MemberRole.USER,
gender: Gender = Gender.NONE
): Member {
return Member(
email = "member$id@test.com",
password = "password",
nickname = "member$id",
gender = gender,
role = role
).also { it.id = id }
}
private inline fun <reified T> mock(): T {
return Mockito.mock(T::class.java)
}
}