fix(home-following): inbox 중복 insert 처리를 보강한다
This commit is contained in:
+66
@@ -0,0 +1,66 @@
|
||||
package kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.FollowingNewsType
|
||||
import kr.co.vividnext.sodalive.v2.home.following.port.out.HomeFollowingNewsInboxRecord
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito
|
||||
import org.springframework.dao.DataIntegrityViolationException
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
class HomeFollowingNewsInboxPersistenceAdapterRetryTest {
|
||||
@Test
|
||||
@DisplayName("insertIgnoreAll은 JPA bulk insert unique 충돌 시 기존 row를 재조회하고 남은 row만 재시도한다")
|
||||
fun shouldRetryRemainingRowsWhenBulkInsertConflictsWithExistingRow() {
|
||||
val repository = Mockito.mock(HomeFollowingNewsInboxJpaRepository::class.java)
|
||||
val entityManager = Mockito.mock(EntityManager::class.java)
|
||||
val adapter = HomeFollowingNewsInboxPersistenceAdapter(repository, entityManager)
|
||||
val sourceKey = "CREATOR_RANKING:1:2026-06-25"
|
||||
Mockito.`when`(
|
||||
repository.findExistingMemberIds(
|
||||
FollowingNewsType.CREATOR_RANKING.name,
|
||||
sourceKey,
|
||||
listOf(10L)
|
||||
)
|
||||
).thenReturn(emptyList()).thenReturn(listOf(10L))
|
||||
Mockito.`when`(repository.saveAll(Mockito.anyList<HomeFollowingNewsInbox>()))
|
||||
.thenThrow(DataIntegrityViolationException("duplicate"))
|
||||
|
||||
val insertedCount = adapter.insertIgnoreAll(
|
||||
listOf(record(memberId = 10L, creatorId = 1L, sourceKey = sourceKey))
|
||||
)
|
||||
|
||||
assertEquals(0, insertedCount)
|
||||
Mockito.verify(repository, Mockito.times(2)).findExistingMemberIds(
|
||||
FollowingNewsType.CREATOR_RANKING.name,
|
||||
sourceKey,
|
||||
listOf(10L)
|
||||
)
|
||||
Mockito.verify(repository, Mockito.times(1)).saveAll(Mockito.anyList<HomeFollowingNewsInbox>())
|
||||
}
|
||||
|
||||
private fun record(
|
||||
memberId: Long,
|
||||
creatorId: Long,
|
||||
sourceKey: String
|
||||
): HomeFollowingNewsInboxRecord {
|
||||
return HomeFollowingNewsInboxRecord(
|
||||
memberId = memberId,
|
||||
creatorId = creatorId,
|
||||
newsType = FollowingNewsType.CREATOR_RANKING.name,
|
||||
sourceKey = sourceKey,
|
||||
targetId = creatorId,
|
||||
occurredAtUtc = LocalDateTime.of(2026, 6, 25, 0, 0),
|
||||
visibleFromAtUtc = LocalDateTime.of(2026, 6, 25, 9, 0),
|
||||
creatorNickname = "creator-$creatorId",
|
||||
creatorProfileImagePath = "profile-$creatorId.png",
|
||||
title = "title",
|
||||
body = "body",
|
||||
thumbnailImagePath = null,
|
||||
rank = 1,
|
||||
isAdult = false
|
||||
)
|
||||
}
|
||||
}
|
||||
+15
-19
@@ -6,16 +6,17 @@ import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.FollowingNewsType
|
||||
import kr.co.vividnext.sodalive.v2.home.following.port.out.HomeFollowingNewsInboxRecord
|
||||
import org.junit.jupiter.api.Assertions.assertDoesNotThrow
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.context.annotation.Import
|
||||
import org.springframework.dao.DataIntegrityViolationException
|
||||
import org.springframework.test.annotation.DirtiesContext
|
||||
import org.springframework.test.context.transaction.TestTransaction
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@@ -48,26 +49,21 @@ class HomeFollowingNewsInboxPersistenceAdapterTest @Autowired constructor(
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("insertIgnoreAll은 exists 확인 이후 발생한 중복 insert 충돌도 예외 없이 무시한다")
|
||||
fun shouldIgnoreDuplicateInsertRaceAfterExistsCheck() {
|
||||
val mockRepository = Mockito.mock(HomeFollowingNewsInboxJpaRepository::class.java)
|
||||
val mockEntityManager = Mockito.mock(EntityManager::class.java)
|
||||
val raceAdapter = HomeFollowingNewsInboxPersistenceAdapter(mockRepository, mockEntityManager)
|
||||
val record = record(sourceKey = "race-source-key")
|
||||
Mockito.`when`(
|
||||
mockRepository.existsByMemberIdAndNewsTypeAndSourceKey(
|
||||
record.memberId,
|
||||
FollowingNewsType.CREATOR_RANKING,
|
||||
record.sourceKey
|
||||
)
|
||||
).thenReturn(false, true)
|
||||
Mockito.`when`(mockRepository.saveAndFlush(Mockito.any(HomeFollowingNewsInbox::class.java)))
|
||||
.thenThrow(DataIntegrityViolationException("duplicate"))
|
||||
@DirtiesContext(methodMode = DirtiesContext.MethodMode.AFTER_METHOD)
|
||||
@DisplayName("실제 unique 중복 무시 이후 insertIgnoreAll을 호출한 트랜잭션은 커밋 가능하다")
|
||||
fun shouldCommitTransactionAfterRealDuplicateCollisionIsIgnored() {
|
||||
val sourceKey = "real-duplicate"
|
||||
adapter.insertIgnoreAll(listOf(record(sourceKey = sourceKey)))
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
val insertCount = raceAdapter.insertIgnoreAll(listOf(record))
|
||||
val insertCount = adapter.insertIgnoreAll(listOf(record(sourceKey = sourceKey)))
|
||||
val rows = repository.findAll()
|
||||
|
||||
assertEquals(0, insertCount)
|
||||
Mockito.verify(mockEntityManager).clear()
|
||||
assertEquals(1, rows.size)
|
||||
TestTransaction.flagForCommit()
|
||||
assertDoesNotThrow { TestTransaction.end() }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user