feat(agent-assignment): 에이전트 크리에이터 소속 관리 기능을 추가한다
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
package kr.co.vividnext.sodalive.admin.partner.agent.assignment
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
|
import kr.co.vividnext.sodalive.partner.agent.assignment.AssignAgentCreatorRequest
|
||||||
|
import kr.co.vividnext.sodalive.partner.agent.assignment.RemoveAgentCreatorRequest
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
@RequestMapping("/admin/partner/agent/assignment")
|
||||||
|
class AdminAgentCreatorController(private val service: AdminAgentCreatorService) {
|
||||||
|
@PostMapping
|
||||||
|
fun assignCreator(@RequestBody request: AssignAgentCreatorRequest) = ApiResponse.ok(service.assignCreator(request))
|
||||||
|
|
||||||
|
@PostMapping("/remove")
|
||||||
|
fun removeCreator(@RequestBody request: RemoveAgentCreatorRequest) = ApiResponse.ok(service.removeCreator(request))
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package kr.co.vividnext.sodalive.admin.partner.agent.assignment
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import kr.co.vividnext.sodalive.partner.agent.assignment.AgentCreatorRelation
|
||||||
|
import kr.co.vividnext.sodalive.partner.agent.assignment.AgentCreatorRelationRepository
|
||||||
|
import kr.co.vividnext.sodalive.partner.agent.assignment.AssignAgentCreatorRequest
|
||||||
|
import kr.co.vividnext.sodalive.partner.agent.assignment.RemoveAgentCreatorRequest
|
||||||
|
import org.springframework.dao.DataIntegrityViolationException
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class AdminAgentCreatorService(
|
||||||
|
private val relationRepository: AgentCreatorRelationRepository,
|
||||||
|
private val memberRepository: MemberRepository
|
||||||
|
) {
|
||||||
|
@Transactional
|
||||||
|
fun assignCreator(request: AssignAgentCreatorRequest) {
|
||||||
|
if (request.agentId == request.creatorId) {
|
||||||
|
throw SodaException(messageKey = "partner.agent.assignment.invalid_relation")
|
||||||
|
}
|
||||||
|
|
||||||
|
val agent = memberRepository.findByIdOrNull(request.agentId)
|
||||||
|
?: throw SodaException(messageKey = "partner.agent.assignment.agent_not_found")
|
||||||
|
if (agent.role != MemberRole.AGENT) {
|
||||||
|
throw SodaException(messageKey = "partner.agent.assignment.invalid_agent")
|
||||||
|
}
|
||||||
|
|
||||||
|
val creator = memberRepository.findByIdForUpdate(request.creatorId)
|
||||||
|
?: throw SodaException(messageKey = "partner.agent.assignment.creator_not_found")
|
||||||
|
if (creator.role != MemberRole.CREATOR) {
|
||||||
|
throw SodaException(messageKey = "partner.agent.assignment.invalid_creator")
|
||||||
|
}
|
||||||
|
|
||||||
|
val existingRelations = relationRepository.findAllByCreatorIdOrderByAssignedAtAsc(request.creatorId)
|
||||||
|
if (hasAssignmentOverlap(existingRelations, request.assignedAt)) {
|
||||||
|
throw SodaException(messageKey = "partner.agent.assignment.assignment_overlap")
|
||||||
|
}
|
||||||
|
|
||||||
|
val relation = AgentCreatorRelation()
|
||||||
|
relation.agent = agent
|
||||||
|
relation.creator = creator
|
||||||
|
relation.assignedAt = request.assignedAt
|
||||||
|
try {
|
||||||
|
relationRepository.saveAndFlush(relation)
|
||||||
|
} catch (e: DataIntegrityViolationException) {
|
||||||
|
relationRepository.findFirstByCreatorIdAndUnassignedAtIsNull(request.creatorId)
|
||||||
|
?: throw e
|
||||||
|
throw SodaException(messageKey = "partner.agent.assignment.assignment_overlap")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun removeCreator(request: RemoveAgentCreatorRequest) {
|
||||||
|
val creator = memberRepository.findByIdForUpdate(request.creatorId)
|
||||||
|
?: throw SodaException(messageKey = "partner.agent.assignment.creator_not_found")
|
||||||
|
if (creator.role != MemberRole.CREATOR) {
|
||||||
|
throw SodaException(messageKey = "partner.agent.assignment.invalid_creator")
|
||||||
|
}
|
||||||
|
|
||||||
|
val relation = relationRepository.findFirstByCreatorIdAndUnassignedAtIsNull(request.creatorId)
|
||||||
|
?: throw SodaException(messageKey = "partner.agent.assignment.not_found")
|
||||||
|
|
||||||
|
val assignedAt = relation.assignedAt
|
||||||
|
?: throw SodaException(messageKey = "partner.agent.assignment.not_found")
|
||||||
|
if (!request.unassignedAt.isAfter(assignedAt)) {
|
||||||
|
throw SodaException(messageKey = "partner.agent.assignment.invalid_unassigned_at")
|
||||||
|
}
|
||||||
|
|
||||||
|
relation.unassignedAt = request.unassignedAt
|
||||||
|
relationRepository.save(relation)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasAssignmentOverlap(
|
||||||
|
existingRelations: List<AgentCreatorRelation>,
|
||||||
|
assignedAt: LocalDateTime
|
||||||
|
): Boolean {
|
||||||
|
return existingRelations.any { relation ->
|
||||||
|
val existingUnassignedAt = relation.unassignedAt
|
||||||
|
existingUnassignedAt == null || assignedAt.isBefore(existingUnassignedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package kr.co.vividnext.sodalive.partner.agent.assignment
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import javax.persistence.Column
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.FetchType
|
||||||
|
import javax.persistence.JoinColumn
|
||||||
|
import javax.persistence.ManyToOne
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
class AgentCreatorRelation : BaseEntity() {
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "agent_id", nullable = false)
|
||||||
|
var agent: Member? = null
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "creator_id", nullable = false)
|
||||||
|
var creator: Member? = null
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
var assignedAt: LocalDateTime? = null
|
||||||
|
|
||||||
|
@Column(nullable = true)
|
||||||
|
var unassignedAt: LocalDateTime? = null
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package kr.co.vividnext.sodalive.partner.agent.assignment
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
|
||||||
|
interface AgentCreatorRelationRepository : JpaRepository<AgentCreatorRelation, Long> {
|
||||||
|
fun findAllByCreatorIdOrderByAssignedAtAsc(creatorId: Long): List<AgentCreatorRelation>
|
||||||
|
|
||||||
|
fun findFirstByCreatorIdAndUnassignedAtIsNull(creatorId: Long): AgentCreatorRelation?
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package kr.co.vividnext.sodalive.partner.agent.assignment
|
||||||
|
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
data class AssignAgentCreatorRequest(
|
||||||
|
val agentId: Long,
|
||||||
|
val creatorId: Long,
|
||||||
|
val assignedAt: LocalDateTime
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package kr.co.vividnext.sodalive.partner.agent.assignment
|
||||||
|
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
data class RemoveAgentCreatorRequest(
|
||||||
|
val creatorId: Long,
|
||||||
|
val unassignedAt: LocalDateTime
|
||||||
|
)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package kr.co.vividnext.sodalive.admin.partner.agent.assignment
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.partner.agent.assignment.AssignAgentCreatorRequest
|
||||||
|
import kr.co.vividnext.sodalive.partner.agent.assignment.RemoveAgentCreatorRequest
|
||||||
|
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.mockito.Mockito
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
class AdminAgentCreatorControllerTest {
|
||||||
|
private lateinit var service: AdminAgentCreatorService
|
||||||
|
private lateinit var controller: AdminAgentCreatorController
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
service = Mockito.mock(AdminAgentCreatorService::class.java)
|
||||||
|
controller = AdminAgentCreatorController(service)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("관리자 컨트롤러는 소속 지정 요청을 서비스로 전달한다")
|
||||||
|
fun shouldForwardAssignRequestToService() {
|
||||||
|
val request = AssignAgentCreatorRequest(
|
||||||
|
agentId = 11L,
|
||||||
|
creatorId = 22L,
|
||||||
|
assignedAt = LocalDateTime.of(2026, 4, 9, 10, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = controller.assignCreator(request)
|
||||||
|
|
||||||
|
assertEquals(true, response.success)
|
||||||
|
Mockito.verify(service).assignCreator(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("관리자 컨트롤러는 소속 해제 요청을 서비스로 전달한다")
|
||||||
|
fun shouldForwardRemoveRequestToService() {
|
||||||
|
val request = RemoveAgentCreatorRequest(
|
||||||
|
creatorId = 22L,
|
||||||
|
unassignedAt = LocalDateTime.of(2026, 4, 9, 11, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = controller.removeCreator(request)
|
||||||
|
|
||||||
|
assertEquals(true, response.success)
|
||||||
|
Mockito.verify(service).removeCreator(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
package kr.co.vividnext.sodalive.admin.partner.agent.assignment
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
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.partner.agent.assignment.AgentCreatorRelation
|
||||||
|
import kr.co.vividnext.sodalive.partner.agent.assignment.AgentCreatorRelationRepository
|
||||||
|
import kr.co.vividnext.sodalive.partner.agent.assignment.AssignAgentCreatorRequest
|
||||||
|
import kr.co.vividnext.sodalive.partner.agent.assignment.RemoveAgentCreatorRequest
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.ArgumentCaptor
|
||||||
|
import org.mockito.InOrder
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.springframework.dao.DataIntegrityViolationException
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
|
class AdminAgentCreatorServiceTest {
|
||||||
|
private lateinit var relationRepository: AgentCreatorRelationRepository
|
||||||
|
private lateinit var memberRepository: MemberRepository
|
||||||
|
private lateinit var service: AdminAgentCreatorService
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
relationRepository = Mockito.mock(AgentCreatorRelationRepository::class.java)
|
||||||
|
memberRepository = Mockito.mock(MemberRepository::class.java)
|
||||||
|
service = AdminAgentCreatorService(
|
||||||
|
relationRepository = relationRepository,
|
||||||
|
memberRepository = memberRepository
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("관리자는 에이전트와 크리에이터를 소속으로 연결할 수 있다")
|
||||||
|
fun shouldAssignCreatorToAgent() {
|
||||||
|
val assignedAt = LocalDateTime.of(2026, 4, 9, 10, 0)
|
||||||
|
val agent = Member(password = "password", nickname = "agent", role = MemberRole.AGENT)
|
||||||
|
agent.id = 11L
|
||||||
|
val creator = Member(password = "password", nickname = "creator", role = MemberRole.CREATOR)
|
||||||
|
creator.id = 22L
|
||||||
|
val request = AssignAgentCreatorRequest(agentId = 11L, creatorId = 22L, assignedAt = assignedAt)
|
||||||
|
|
||||||
|
Mockito.`when`(memberRepository.findById(11L)).thenReturn(Optional.of(agent))
|
||||||
|
Mockito.`when`(memberRepository.findByIdForUpdate(22L)).thenReturn(creator)
|
||||||
|
Mockito.`when`(relationRepository.findAllByCreatorIdOrderByAssignedAtAsc(22L)).thenReturn(emptyList())
|
||||||
|
|
||||||
|
service.assignCreator(request)
|
||||||
|
|
||||||
|
val relationCaptor = ArgumentCaptor.forClass(AgentCreatorRelation::class.java)
|
||||||
|
Mockito.verify(relationRepository).saveAndFlush(relationCaptor.capture())
|
||||||
|
assertEquals(agent, relationCaptor.value.agent)
|
||||||
|
assertEquals(creator, relationCaptor.value.creator)
|
||||||
|
assertEquals(assignedAt, relationCaptor.value.assignedAt)
|
||||||
|
assertEquals(null, relationCaptor.value.unassignedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("관리자 소속 지정은 creator row를 잠근 뒤 저장한다")
|
||||||
|
fun shouldLockCreatorBeforeAssigning() {
|
||||||
|
val assignedAt = LocalDateTime.of(2026, 4, 9, 10, 0)
|
||||||
|
val agent = Member(password = "password", nickname = "agent", role = MemberRole.AGENT)
|
||||||
|
agent.id = 11L
|
||||||
|
val creator = Member(password = "password", nickname = "creator", role = MemberRole.CREATOR)
|
||||||
|
creator.id = 22L
|
||||||
|
val request = AssignAgentCreatorRequest(agentId = 11L, creatorId = 22L, assignedAt = assignedAt)
|
||||||
|
|
||||||
|
Mockito.`when`(memberRepository.findById(11L)).thenReturn(Optional.of(agent))
|
||||||
|
Mockito.`when`(memberRepository.findByIdForUpdate(22L)).thenReturn(creator)
|
||||||
|
Mockito.`when`(relationRepository.findAllByCreatorIdOrderByAssignedAtAsc(22L)).thenReturn(emptyList())
|
||||||
|
|
||||||
|
service.assignCreator(request)
|
||||||
|
|
||||||
|
val inOrder: InOrder = Mockito.inOrder(memberRepository, relationRepository)
|
||||||
|
inOrder.verify(memberRepository).findById(11L)
|
||||||
|
inOrder.verify(memberRepository).findByIdForUpdate(22L)
|
||||||
|
inOrder.verify(relationRepository).findAllByCreatorIdOrderByAssignedAtAsc(22L)
|
||||||
|
inOrder.verify(relationRepository).saveAndFlush(Mockito.any(AgentCreatorRelation::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("동시 요청으로 소속 unique 제약이 충돌하면 중복 소속 예외로 변환한다")
|
||||||
|
fun shouldThrowAssignmentOverlapWhenConcurrentInsertViolatesUniqueConstraint() {
|
||||||
|
val assignedAt = LocalDateTime.of(2026, 4, 9, 10, 0)
|
||||||
|
val agent = Member(password = "password", nickname = "agent", role = MemberRole.AGENT)
|
||||||
|
agent.id = 11L
|
||||||
|
val creator = Member(password = "password", nickname = "creator", role = MemberRole.CREATOR)
|
||||||
|
creator.id = 22L
|
||||||
|
val existingRelation = AgentCreatorRelation()
|
||||||
|
existingRelation.agent = agent
|
||||||
|
existingRelation.creator = creator
|
||||||
|
existingRelation.assignedAt = LocalDateTime.of(2026, 4, 9, 10, 0)
|
||||||
|
val request = AssignAgentCreatorRequest(agentId = 11L, creatorId = 22L, assignedAt = assignedAt)
|
||||||
|
|
||||||
|
Mockito.`when`(memberRepository.findById(11L)).thenReturn(Optional.of(agent))
|
||||||
|
Mockito.`when`(memberRepository.findByIdForUpdate(22L)).thenReturn(creator)
|
||||||
|
Mockito.`when`(relationRepository.findAllByCreatorIdOrderByAssignedAtAsc(22L)).thenReturn(emptyList())
|
||||||
|
Mockito.`when`(relationRepository.saveAndFlush(Mockito.any(AgentCreatorRelation::class.java)))
|
||||||
|
.thenThrow(DataIntegrityViolationException("duplicate"))
|
||||||
|
Mockito.`when`(relationRepository.findFirstByCreatorIdAndUnassignedAtIsNull(22L)).thenReturn(existingRelation)
|
||||||
|
|
||||||
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
|
service.assignCreator(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("partner.agent.assignment.assignment_overlap", exception.messageKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("에이전트가 아니면 크리에이터를 소속으로 지정할 수 없다")
|
||||||
|
fun shouldThrowWhenMemberIsNotAgent() {
|
||||||
|
val invalidAgent = Member(password = "password", nickname = "user", role = MemberRole.USER)
|
||||||
|
invalidAgent.id = 11L
|
||||||
|
val request = AssignAgentCreatorRequest(
|
||||||
|
agentId = 11L,
|
||||||
|
creatorId = 22L,
|
||||||
|
assignedAt = LocalDateTime.of(2026, 4, 9, 10, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
Mockito.`when`(memberRepository.findById(11L)).thenReturn(Optional.of(invalidAgent))
|
||||||
|
|
||||||
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
|
service.assignCreator(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("partner.agent.assignment.invalid_agent", exception.messageKey)
|
||||||
|
Mockito.verify(memberRepository).findById(11L)
|
||||||
|
Mockito.verifyNoInteractions(relationRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("에이전트 회원이 없으면 소속 지정을 진행할 수 없다")
|
||||||
|
fun shouldThrowWhenAgentDoesNotExist() {
|
||||||
|
val request = AssignAgentCreatorRequest(
|
||||||
|
agentId = 11L,
|
||||||
|
creatorId = 22L,
|
||||||
|
assignedAt = LocalDateTime.of(2026, 4, 9, 10, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
Mockito.`when`(memberRepository.findById(11L)).thenReturn(Optional.empty())
|
||||||
|
|
||||||
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
|
service.assignCreator(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("partner.agent.assignment.agent_not_found", exception.messageKey)
|
||||||
|
Mockito.verify(memberRepository).findById(11L)
|
||||||
|
Mockito.verifyNoInteractions(relationRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("크리에이터가 아니면 에이전트에 소속시킬 수 없다")
|
||||||
|
fun shouldThrowWhenMemberIsNotCreator() {
|
||||||
|
val agent = Member(password = "password", nickname = "agent", role = MemberRole.AGENT)
|
||||||
|
agent.id = 11L
|
||||||
|
val invalidCreator = Member(password = "password", nickname = "user", role = MemberRole.USER)
|
||||||
|
invalidCreator.id = 22L
|
||||||
|
val request = AssignAgentCreatorRequest(
|
||||||
|
agentId = 11L,
|
||||||
|
creatorId = 22L,
|
||||||
|
assignedAt = LocalDateTime.of(2026, 4, 9, 10, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
Mockito.`when`(memberRepository.findById(11L)).thenReturn(Optional.of(agent))
|
||||||
|
Mockito.`when`(memberRepository.findByIdForUpdate(22L)).thenReturn(invalidCreator)
|
||||||
|
|
||||||
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
|
service.assignCreator(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("partner.agent.assignment.invalid_creator", exception.messageKey)
|
||||||
|
Mockito.verify(memberRepository).findById(11L)
|
||||||
|
Mockito.verify(memberRepository).findByIdForUpdate(22L)
|
||||||
|
Mockito.verifyNoInteractions(relationRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("크리에이터 회원이 없으면 소속 지정을 진행할 수 없다")
|
||||||
|
fun shouldThrowWhenCreatorDoesNotExist() {
|
||||||
|
val agent = Member(password = "password", nickname = "agent", role = MemberRole.AGENT)
|
||||||
|
agent.id = 11L
|
||||||
|
val request = AssignAgentCreatorRequest(
|
||||||
|
agentId = 11L,
|
||||||
|
creatorId = 22L,
|
||||||
|
assignedAt = LocalDateTime.of(2026, 4, 9, 10, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
Mockito.`when`(memberRepository.findById(11L)).thenReturn(Optional.of(agent))
|
||||||
|
Mockito.`when`(memberRepository.findByIdForUpdate(22L)).thenReturn(null)
|
||||||
|
|
||||||
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
|
service.assignCreator(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("partner.agent.assignment.creator_not_found", exception.messageKey)
|
||||||
|
Mockito.verify(memberRepository).findById(11L)
|
||||||
|
Mockito.verify(memberRepository).findByIdForUpdate(22L)
|
||||||
|
Mockito.verifyNoInteractions(relationRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("이미 다른 에이전트에 소속된 크리에이터는 중복 지정할 수 없다")
|
||||||
|
fun shouldThrowWhenCreatorIsAlreadyAssigned() {
|
||||||
|
val assignedAt = LocalDateTime.of(2026, 4, 9, 10, 0)
|
||||||
|
val agent = Member(password = "password", nickname = "agent", role = MemberRole.AGENT)
|
||||||
|
agent.id = 11L
|
||||||
|
val creator = Member(password = "password", nickname = "creator", role = MemberRole.CREATOR)
|
||||||
|
creator.id = 22L
|
||||||
|
val existingRelation = AgentCreatorRelation()
|
||||||
|
existingRelation.agent = agent
|
||||||
|
existingRelation.creator = creator
|
||||||
|
existingRelation.assignedAt = LocalDateTime.of(2026, 4, 1, 0, 0)
|
||||||
|
existingRelation.unassignedAt = null
|
||||||
|
val request = AssignAgentCreatorRequest(agentId = 11L, creatorId = 22L, assignedAt = assignedAt)
|
||||||
|
|
||||||
|
Mockito.`when`(memberRepository.findById(11L)).thenReturn(Optional.of(agent))
|
||||||
|
Mockito.`when`(memberRepository.findByIdForUpdate(22L)).thenReturn(creator)
|
||||||
|
Mockito.`when`(relationRepository.findAllByCreatorIdOrderByAssignedAtAsc(22L)).thenReturn(listOf(existingRelation))
|
||||||
|
|
||||||
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
|
service.assignCreator(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("partner.agent.assignment.assignment_overlap", exception.messageKey)
|
||||||
|
Mockito.verify(relationRepository).findAllByCreatorIdOrderByAssignedAtAsc(22L)
|
||||||
|
Mockito.verify(relationRepository, Mockito.never()).save(Mockito.any(AgentCreatorRelation::class.java))
|
||||||
|
Mockito.verify(relationRepository, Mockito.never()).saveAndFlush(Mockito.any(AgentCreatorRelation::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("종료된 이력이 있으면 이후 시각에 다시 소속 지정할 수 있다")
|
||||||
|
fun shouldAssignCreatorWhenPreviousAssignmentAlreadyEnded() {
|
||||||
|
val assignedAt = LocalDateTime.of(2026, 4, 9, 10, 0)
|
||||||
|
val agent = Member(password = "password", nickname = "agent", role = MemberRole.AGENT)
|
||||||
|
agent.id = 11L
|
||||||
|
val creator = Member(password = "password", nickname = "creator", role = MemberRole.CREATOR)
|
||||||
|
creator.id = 22L
|
||||||
|
val existingRelation = AgentCreatorRelation()
|
||||||
|
existingRelation.agent = agent
|
||||||
|
existingRelation.creator = creator
|
||||||
|
existingRelation.assignedAt = LocalDateTime.of(2026, 4, 1, 0, 0)
|
||||||
|
existingRelation.unassignedAt = assignedAt
|
||||||
|
val request = AssignAgentCreatorRequest(agentId = 11L, creatorId = 22L, assignedAt = assignedAt)
|
||||||
|
|
||||||
|
Mockito.`when`(memberRepository.findById(11L)).thenReturn(Optional.of(agent))
|
||||||
|
Mockito.`when`(memberRepository.findByIdForUpdate(22L)).thenReturn(creator)
|
||||||
|
Mockito.`when`(relationRepository.findAllByCreatorIdOrderByAssignedAtAsc(22L)).thenReturn(listOf(existingRelation))
|
||||||
|
|
||||||
|
service.assignCreator(request)
|
||||||
|
|
||||||
|
Mockito.verify(relationRepository).saveAndFlush(Mockito.any(AgentCreatorRelation::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("관리자는 활성 소속의 종료 시각을 기록해 크리에이터 소속을 해제할 수 있다")
|
||||||
|
fun shouldCloseCreatorAssignmentWindow() {
|
||||||
|
val relation = AgentCreatorRelation()
|
||||||
|
relation.assignedAt = LocalDateTime.of(2026, 4, 1, 0, 0)
|
||||||
|
val request = RemoveAgentCreatorRequest(
|
||||||
|
creatorId = 22L,
|
||||||
|
unassignedAt = LocalDateTime.of(2026, 4, 9, 10, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
Mockito.`when`(relationRepository.findFirstByCreatorIdAndUnassignedAtIsNull(22L)).thenReturn(relation)
|
||||||
|
Mockito.`when`(memberRepository.findByIdForUpdate(22L)).thenReturn(
|
||||||
|
Member(
|
||||||
|
password = "password",
|
||||||
|
nickname = "creator",
|
||||||
|
role = MemberRole.CREATOR
|
||||||
|
).also { it.id = 22L }
|
||||||
|
)
|
||||||
|
|
||||||
|
service.removeCreator(request)
|
||||||
|
|
||||||
|
assertEquals(request.unassignedAt, relation.unassignedAt)
|
||||||
|
Mockito.verify(relationRepository).save(relation)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("소속 정보가 없으면 해제할 수 없다")
|
||||||
|
fun shouldThrowWhenAssignmentDoesNotExist() {
|
||||||
|
val request = RemoveAgentCreatorRequest(
|
||||||
|
creatorId = 22L,
|
||||||
|
unassignedAt = LocalDateTime.of(2026, 4, 9, 10, 0)
|
||||||
|
)
|
||||||
|
Mockito.`when`(memberRepository.findByIdForUpdate(22L)).thenReturn(
|
||||||
|
Member(
|
||||||
|
password = "password",
|
||||||
|
nickname = "creator",
|
||||||
|
role = MemberRole.CREATOR
|
||||||
|
).also { it.id = 22L }
|
||||||
|
)
|
||||||
|
Mockito.`when`(relationRepository.findFirstByCreatorIdAndUnassignedAtIsNull(22L)).thenReturn(null)
|
||||||
|
|
||||||
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
|
service.removeCreator(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("partner.agent.assignment.not_found", exception.messageKey)
|
||||||
|
Mockito.verify(relationRepository).findFirstByCreatorIdAndUnassignedAtIsNull(22L)
|
||||||
|
Mockito.verify(relationRepository, Mockito.never()).save(Mockito.any(AgentCreatorRelation::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("소속 종료 시각은 시작 시각보다 늦어야 한다")
|
||||||
|
fun shouldThrowWhenUnassignedAtIsNotAfterAssignedAt() {
|
||||||
|
val relation = AgentCreatorRelation()
|
||||||
|
relation.assignedAt = LocalDateTime.of(2026, 4, 9, 10, 0)
|
||||||
|
val request = RemoveAgentCreatorRequest(
|
||||||
|
creatorId = 22L,
|
||||||
|
unassignedAt = LocalDateTime.of(2026, 4, 9, 10, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
Mockito.`when`(memberRepository.findByIdForUpdate(22L)).thenReturn(
|
||||||
|
Member(
|
||||||
|
password = "password",
|
||||||
|
nickname = "creator",
|
||||||
|
role = MemberRole.CREATOR
|
||||||
|
).also { it.id = 22L }
|
||||||
|
)
|
||||||
|
Mockito.`when`(relationRepository.findFirstByCreatorIdAndUnassignedAtIsNull(22L)).thenReturn(relation)
|
||||||
|
|
||||||
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
|
service.removeCreator(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("partner.agent.assignment.invalid_unassigned_at", exception.messageKey)
|
||||||
|
Mockito.verify(relationRepository, Mockito.never()).save(Mockito.any(AgentCreatorRelation::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user