test #426

Merged
klaus merged 415 commits from test into main 2026-06-27 00:35:30 +00:00
3 changed files with 370 additions and 0 deletions
Showing only changes of commit 408a342f17 - Show all commits

View File

@@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.adapter.out.persistence
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkQueryPort
interface CreatorChannelFanTalkQueryRepository : CreatorChannelFanTalkQueryPort

View File

@@ -0,0 +1,138 @@
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.adapter.out.persistence
import com.querydsl.core.types.Projections
import com.querydsl.core.types.dsl.BooleanExpression
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.explorer.profile.QCreatorCheers.creatorCheers
import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.member.block.QBlockMember
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkCreatorRecord
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkRecord
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkReplyRecord
import org.springframework.stereotype.Repository
@Repository
class DefaultCreatorChannelFanTalkQueryRepository(
private val queryFactory: JPAQueryFactory
) : CreatorChannelFanTalkQueryRepository {
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelFanTalkCreatorRecord? {
val creator = queryFactory
.select(member.id, member.role, member.nickname)
.from(member)
.where(
member.id.eq(creatorId),
member.isActive.isTrue
)
.fetchFirst() ?: return null
return CreatorChannelFanTalkCreatorRecord(
creatorId = creator.get(member.id)!!,
role = creator.get(member.role)!!,
nickname = creator.get(member.nickname)!!
)
}
override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean {
val blockMember = QBlockMember("creatorChannelFanTalkBlockMember")
return queryFactory
.select(blockMember.id)
.from(blockMember)
.where(
blockMember.isActive.isTrue,
blockMember.member.id.eq(viewerId).and(blockMember.blockedMember.id.eq(creatorId))
.or(blockMember.member.id.eq(creatorId).and(blockMember.blockedMember.id.eq(viewerId)))
)
.fetchFirst() != null
}
override fun countFanTalks(creatorId: Long, viewerId: Long): Int {
return queryFactory
.select(creatorCheers.id.count())
.from(creatorCheers)
.where(fanTalkCondition(creatorId, viewerId))
.fetchOne()
?.toInt()
?: 0
}
override fun findFanTalks(
creatorId: Long,
viewerId: Long,
offset: Long,
limit: Int
): List<CreatorChannelFanTalkRecord> {
return queryFactory
.select(
Projections.constructor(
CreatorChannelFanTalkRecord::class.java,
creatorCheers.id,
creatorCheers.member.id,
creatorCheers.member.nickname,
creatorCheers.member.profileImage,
creatorCheers.cheers,
creatorCheers.createdAt
)
)
.from(creatorCheers)
.where(fanTalkCondition(creatorId, viewerId))
.orderBy(creatorCheers.createdAt.desc(), creatorCheers.id.desc())
.offset(offset)
.limit(limit.toLong())
.fetch()
}
override fun findCreatorReplies(
creatorId: Long,
parentFanTalkIds: List<Long>
): List<CreatorChannelFanTalkReplyRecord> {
if (parentFanTalkIds.isEmpty()) return emptyList()
return queryFactory
.select(
Projections.constructor(
CreatorChannelFanTalkReplyRecord::class.java,
creatorCheers.id,
creatorCheers.parent.id,
creatorCheers.member.id,
creatorCheers.member.nickname,
creatorCheers.member.profileImage,
creatorCheers.cheers,
creatorCheers.createdAt
)
)
.from(creatorCheers)
.where(
creatorCheers.creator.id.eq(creatorId),
creatorCheers.member.id.eq(creatorId),
creatorCheers.isActive.isTrue,
creatorCheers.parent.id.`in`(parentFanTalkIds)
)
.orderBy(creatorCheers.createdAt.asc(), creatorCheers.id.asc())
.fetch()
}
private fun fanTalkCondition(creatorId: Long, viewerId: Long): BooleanExpression {
return creatorCheers.creator.id.eq(creatorId)
.and(creatorCheers.isActive.isTrue)
.and(creatorCheers.parent.isNull)
.and(notBlockedFanTalkWriterCondition(viewerId))
}
private fun notBlockedFanTalkWriterCondition(viewerId: Long): BooleanExpression {
val viewerBlock = QBlockMember("viewerBlockFanTalkTabWriter")
val writerBlock = QBlockMember("writerBlockFanTalkTabViewer")
return creatorCheers.member.id.notIn(
queryFactory
.select(viewerBlock.blockedMember.id)
.from(viewerBlock)
.where(viewerBlock.member.id.eq(viewerId), viewerBlock.isActive.isTrue)
).and(
creatorCheers.member.id.notIn(
queryFactory
.select(writerBlock.member.id)
.from(writerBlock)
.where(writerBlock.blockedMember.id.eq(viewerId), writerBlock.isActive.isTrue)
)
)
}
}

View File

@@ -0,0 +1,227 @@
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.adapter.out.persistence
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.block.BlockMember
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import java.time.LocalDateTime
import javax.persistence.EntityManager
@DataJpaTest(
properties = [
"spring.cache.type=none",
"spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;NON_KEYWORDS=VALUE"
]
)
@Import(QueryDslConfig::class)
class DefaultCreatorChannelFanTalkQueryRepositoryTest @Autowired constructor(
private val entityManager: EntityManager,
queryFactory: JPAQueryFactory
) {
private val repository = DefaultCreatorChannelFanTalkQueryRepository(queryFactory)
@Test
@DisplayName("활성 회원은 role과 닉네임을 조회하고 비활성 회원은 조회하지 않는다")
fun shouldFindOnlyActiveCreator() {
val viewer = saveMember("creator-lookup-viewer", MemberRole.USER)
val activeCreator = saveMember("active-fantalk-creator", MemberRole.CREATOR)
val inactiveCreator = saveMember("inactive-fantalk-creator", MemberRole.CREATOR, isActive = false)
val nonCreator = saveMember("fantalk-non-creator", MemberRole.USER)
flushAndClear()
val activeRecord = repository.findCreator(activeCreator.id!!, viewer.id!!)
val inactiveRecord = repository.findCreator(inactiveCreator.id!!, viewer.id!!)
val nonCreatorRecord = repository.findCreator(nonCreator.id!!, viewer.id!!)
assertNotNull(activeRecord)
assertEquals(activeCreator.id, activeRecord!!.creatorId)
assertEquals(MemberRole.CREATOR, activeRecord.role)
assertEquals(activeCreator.nickname, activeRecord.nickname)
assertNull(inactiveRecord)
assertEquals(MemberRole.USER, nonCreatorRecord!!.role)
}
@Test
@DisplayName("조회자와 크리에이터 사이 양방향 활성 차단만 차단 상태로 조회한다")
fun shouldFindActiveBlockInBothDirections() {
val viewer = saveMember("fantalk-block-viewer", MemberRole.USER)
val creator = saveMember("fantalk-block-creator", MemberRole.CREATOR)
val otherCreator = saveMember("fantalk-other-creator", MemberRole.CREATOR)
saveBlock(viewer, creator, isActive = true)
saveBlock(otherCreator, viewer, isActive = false)
flushAndClear()
assertTrue(repository.existsBlockedBetween(viewer.id!!, creator.id!!))
assertTrue(repository.existsBlockedBetween(creator.id!!, viewer.id!!))
assertFalse(repository.existsBlockedBetween(viewer.id!!, otherCreator.id!!))
}
@Test
@DisplayName("최상위 FanTalk 수와 목록은 활성 루트 글만 세고 작성자 차단을 제외한다")
fun shouldCountAndFindOnlyVisibleTopLevelFanTalks() {
val viewer = saveMember("fantalk-list-viewer", MemberRole.USER)
val creator = saveMember("fantalk-list-creator", MemberRole.CREATOR)
val otherCreator = saveMember("fantalk-list-other-creator", MemberRole.CREATOR)
val visibleWriter = saveMember("visible-writer", MemberRole.USER, profileImage = "visible.png")
val blockedWriter = saveMember("blocked-writer", MemberRole.USER)
val writerBlockingViewer = saveMember("writer-blocking-viewer", MemberRole.USER)
val now = LocalDateTime.of(2026, 6, 22, 12, 0)
val older = saveCheers(visibleWriter, creator, "older", isActive = true, createdAt = now.minusHours(2))
val newer = saveCheers(visibleWriter, creator, "newer", isActive = true, createdAt = now.minusHours(1))
saveCheers(visibleWriter, creator, "inactive", isActive = false, createdAt = now)
saveCheers(visibleWriter, otherCreator, "other creator", isActive = true, createdAt = now)
saveCheers(visibleWriter, creator, "reply", isActive = true, createdAt = now, parent = older)
saveCheers(blockedWriter, creator, "viewer blocked", isActive = true, createdAt = now.plusHours(1))
saveCheers(writerBlockingViewer, creator, "writer blocked", isActive = true, createdAt = now.plusHours(2))
saveBlock(viewer, blockedWriter, isActive = true)
saveBlock(writerBlockingViewer, viewer, isActive = true)
flushAndClear()
val count = repository.countFanTalks(creator.id!!, viewer.id!!)
val firstPage = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 0, limit = 1)
val secondPage = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 1, limit = 2)
assertEquals(2, count)
assertEquals(listOf(newer.id), firstPage.map { it.fanTalkId })
assertEquals(listOf(older.id), secondPage.map { it.fanTalkId })
assertEquals(visibleWriter.id, firstPage.first().writerId)
assertEquals(visibleWriter.nickname, firstPage.first().writerNickname)
assertEquals(visibleWriter.profileImage, firstPage.first().writerProfileImagePath)
assertEquals("newer", firstPage.first().content)
}
@Test
@DisplayName("최상위 FanTalk 목록은 createdAt desc, id desc 순서로 정렬한다")
fun shouldOrderFanTalksByCreatedAtDescAndIdDesc() {
val viewer = saveMember("fantalk-order-viewer", MemberRole.USER)
val creator = saveMember("fantalk-order-creator", MemberRole.CREATOR)
val writer = saveMember("fantalk-order-writer", MemberRole.USER)
val sameCreatedAt = LocalDateTime.of(2026, 6, 22, 12, 0)
val first = saveCheers(writer, creator, "first", isActive = true, createdAt = sameCreatedAt)
val second = saveCheers(writer, creator, "second", isActive = true, createdAt = sameCreatedAt)
val newest = saveCheers(writer, creator, "newest", isActive = true, createdAt = sameCreatedAt.plusMinutes(1))
flushAndClear()
val records = repository.findFanTalks(creator.id!!, viewer.id!!, offset = 0, limit = 10)
assertEquals(listOf(newest.id, second.id, first.id), records.map { it.fanTalkId })
}
@Test
@DisplayName("크리에이터 답글은 지정한 부모의 활성 크리에이터 작성 답글만 오래된 순으로 조회한다")
fun shouldFindOnlyActiveCreatorRepliesForRequestedParents() {
val creator = saveMember("reply-creator", MemberRole.CREATOR)
val writer = saveMember("reply-writer", MemberRole.USER)
val otherCreator = saveMember("reply-other-creator", MemberRole.CREATOR)
val now = LocalDateTime.of(2026, 6, 22, 12, 0)
val parent = saveCheers(writer, creator, "parent", isActive = true, createdAt = now.minusHours(3))
val otherParent = saveCheers(writer, creator, "other parent", isActive = true, createdAt = now.minusHours(2))
val newerReply = saveCheers(creator, creator, "newer reply", isActive = true, createdAt = now, parent = parent)
val olderReply = saveCheers(
creator,
creator,
"older reply",
isActive = true,
createdAt = now.minusMinutes(1),
parent = parent
)
saveCheers(writer, creator, "fan reply", isActive = true, createdAt = now.plusMinutes(1), parent = parent)
saveCheers(creator, creator, "inactive reply", isActive = false, createdAt = now.plusMinutes(2), parent = parent)
saveCheers(
creator,
otherCreator,
"other creator reply",
isActive = true,
createdAt = now.plusMinutes(3),
parent = parent
)
saveCheers(
creator,
creator,
"other parent reply",
isActive = true,
createdAt = now.plusMinutes(4),
parent = otherParent
)
flushAndClear()
val replies = repository.findCreatorReplies(creator.id!!, listOf(parent.id!!))
val emptyReplies = repository.findCreatorReplies(creator.id!!, emptyList())
assertEquals(listOf(olderReply.id, newerReply.id), replies.map { it.fanTalkId })
assertEquals(parent.id, replies.first().parentFanTalkId)
assertEquals(creator.id, replies.first().writerId)
assertEquals(creator.nickname, replies.first().writerNickname)
assertEquals(creator.profileImage, replies.first().writerProfileImagePath)
assertEquals("older reply", replies.first().content)
assertTrue(emptyReplies.isEmpty())
}
private fun saveMember(
nickname: String,
role: MemberRole,
isActive: Boolean = true,
profileImage: String? = "$nickname.png"
): Member {
val member = Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname,
profileImage = profileImage,
role = role,
isActive = isActive
)
entityManager.persist(member)
return member
}
private fun saveBlock(member: Member, blockedMember: Member, isActive: Boolean): BlockMember {
val block = BlockMember(isActive = isActive)
block.member = member
block.blockedMember = blockedMember
entityManager.persist(block)
return block
}
private fun saveCheers(
member: Member,
creator: Member,
cheers: String,
isActive: Boolean,
createdAt: LocalDateTime,
parent: CreatorCheers? = null
): CreatorCheers {
val creatorCheers = CreatorCheers(cheers = cheers, languageCode = "ko", isActive = isActive)
creatorCheers.member = member
creatorCheers.creator = creator
creatorCheers.parent = parent
entityManager.persist(creatorCheers)
entityManager.flush()
updateCreatedAt("CreatorCheers", creatorCheers.id!!, createdAt)
return creatorCheers
}
private fun updateCreatedAt(entityName: String, id: Long, createdAt: LocalDateTime) {
entityManager.createQuery("update $entityName e set e.createdAt = :createdAt where e.id = :id")
.setParameter("createdAt", createdAt)
.setParameter("id", id)
.executeUpdate()
}
private fun flushAndClear() {
entityManager.flush()
entityManager.clear()
}
}