Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ASAP-110 feat: 편지 열람 가능여부 검증 api 구현 및 테스트 추가 #29

Merged
merged 1 commit into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.asap.application.letter.exception

import com.asap.common.exception.BusinessException

sealed class LetterException(
codePrefix: String = CODE_PREFIX,
errorCode: Int,
httpStatus: Int = 400,
message: String = DEFAULT_ERROR_MESSAGE
): BusinessException(codePrefix, errorCode, httpStatus, message) {

class SendLetterNotFoundException(
message: String = "존재하지 않는 편지입니다."
): LetterException(
errorCode = 1,
message = message
)

class InvalidLetterAccessException(
message: String = "편지에 대한 접근 권한이 없습니다."
): LetterException(
errorCode = 2,
message = message,
httpStatus = 403
)

companion object {
const val CODE_PREFIX = "LETTER"
const val DEFAULT_ERROR_MESSAGE = "편지 관련된 예외가 발생했습니다."
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.asap.application.letter.port.`in`

interface VerifyLetterAccessibleUsecase {

fun verify(
command: Command
): Response


data class Command(
val letterCode: String,
val userId: String
)

data class Response(
val letterId: String
)
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,46 @@
package com.asap.application.letter.port.out

import com.asap.application.letter.exception.LetterException
import com.asap.domain.common.DomainId
import com.asap.domain.letter.entity.SendLetter

interface SendLetterManagementPort {

fun save(
sendLetter: SendLetter
)

@Throws(
LetterException.SendLetterNotFoundException::class
)
fun getLetterNotNull(
letterId: DomainId
): SendLetter

@Throws(
LetterException.SendLetterNotFoundException::class
)
fun getLetterByCodeNotNull(
letterCode: String
): SendLetter

@Throws(
LetterException.SendLetterNotFoundException::class
)
fun getExpiredLetterNotNull(
receiverId: DomainId,
letterCode: String
): SendLetter


fun expireLetter(
receiverId: DomainId,
letterId: DomainId
)

fun verifiedLetter(
receiverId: DomainId,
letterCode: String
): Boolean

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.asap.application.letter.port.out.memory

import com.asap.application.letter.exception.LetterException
import com.asap.application.letter.port.out.SendLetterManagementPort
import com.asap.domain.common.DomainId
import com.asap.domain.letter.entity.SendLetter
import org.springframework.context.annotation.Primary
import org.springframework.stereotype.Component
Expand All @@ -9,11 +11,91 @@ import org.springframework.stereotype.Component
@Primary
class MemorySendLetterManagementAdapter(

): SendLetterManagementPort {
private val sendLetters = mutableListOf<SendLetter>()
) : SendLetterManagementPort {
private val sendLetters = mutableListOf<SendLetterEntity>()


override fun save(sendLetter: SendLetter) {
sendLetters.add(sendLetter)
sendLetters.add(SendLetterEntity.fromSendLetter(sendLetter))
}

override fun getLetterNotNull(letterId: DomainId): SendLetter {
return matchingNotExpired { this.id == letterId.value }?.toSendLetter()
?: throw LetterException.SendLetterNotFoundException()
}

override fun getLetterByCodeNotNull(letterCode: String): SendLetter {
return matchingNotExpired { this.letterCode == letterCode }?.toSendLetter()
?: throw LetterException.SendLetterNotFoundException()
}

override fun getExpiredLetterNotNull(receiverId: DomainId, letterCode: String): SendLetter {
return matching { (this.letterCode == letterCode) and (this.receiverId == receiverId.value) }?.toSendLetter()
?: throw LetterException.SendLetterNotFoundException()
}

override fun expireLetter(receiverId: DomainId, letterId: DomainId) {
val sendLetter = matchingNotExpired { this.id == letterId.value }
?: throw LetterException.SendLetterNotFoundException()
sendLetter.expire(receiverId.value)
}

override fun verifiedLetter(receiverId: DomainId, letterCode: String): Boolean {
return matching { (this.letterCode == letterCode) and (this.receiverId == receiverId.value) }?.isExpired
?: false
}

private fun matchingNotExpired(query: SendLetterEntity.() -> Boolean): SendLetterEntity? {
return matching { this.isExpired.not() and query() }
}

private fun matching(query: SendLetterEntity.() -> Boolean): SendLetterEntity? {
return sendLetters.find { query(it) }
}


data class SendLetterEntity(
val id: String,
val receiverName: String,
val content: String,
val images: List<String>,
val templateType: Int,
val senderId: String,
val letterCode: String,
var isExpired: Boolean,
var receiverId: String
) {
fun toSendLetter(): SendLetter {
return SendLetter(
id = DomainId(id),
receiverName = receiverName,
content = content,
images = images,
templateType = templateType,
senderId = DomainId(senderId),
letterCode = letterCode
)
}

companion object {
fun fromSendLetter(sendLetter: SendLetter): SendLetterEntity {
return SendLetterEntity(
id = sendLetter.id.value,
receiverName = sendLetter.receiverName,
content = sendLetter.content,
images = sendLetter.images,
templateType = sendLetter.templateType,
senderId = sendLetter.senderId.value,
letterCode = sendLetter.letterCode,
isExpired = false,
receiverId = ""
)
}
}

fun expire(receiverId: String) {
this.isExpired = true
this.receiverId = receiverId
}
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package com.asap.application.letter.service

import com.asap.application.letter.exception.LetterException
import com.asap.application.letter.port.`in`.SendLetterUsecase
import com.asap.application.letter.port.`in`.VerifyLetterAccessibleUsecase
import com.asap.application.letter.port.out.SendLetterManagementPort
import com.asap.application.user.port.out.UserManagementPort
import com.asap.domain.common.DomainId
import com.asap.domain.letter.entity.SendLetter
import com.asap.domain.letter.service.LetterCodeGenerator
import org.springframework.stereotype.Service

@Service
class LetterCommandService(
private val sendLetterManagementPort: SendLetterManagementPort
) : SendLetterUsecase {
private val sendLetterManagementPort: SendLetterManagementPort,
private val userManagementPort: UserManagementPort
) : SendLetterUsecase, VerifyLetterAccessibleUsecase {

private val letterCodeGenerator = LetterCodeGenerator()

Expand All @@ -35,4 +39,25 @@ class LetterCommandService(

return SendLetterUsecase.Response(letterCode = sendLetter.letterCode)
}

override fun verify(command: VerifyLetterAccessibleUsecase.Command): VerifyLetterAccessibleUsecase.Response {
if (sendLetterManagementPort.verifiedLetter(DomainId(command.userId), command.letterCode)) {
val sendLetter = sendLetterManagementPort.getExpiredLetterNotNull(
receiverId = DomainId(command.userId),
command.letterCode
)
return VerifyLetterAccessibleUsecase.Response(letterId = sendLetter.id.value)
}

val sendLetter = sendLetterManagementPort.getLetterByCodeNotNull(command.letterCode)
sendLetter.isSameReceiver {
userManagementPort.getUserNotNull(DomainId(command.userId)).username
}.takeIf { it }?.let {
sendLetterManagementPort.expireLetter(
receiverId = DomainId(command.userId),
letterId = sendLetter.id
)
return VerifyLetterAccessibleUsecase.Response(letterId = sendLetter.id.value)
} ?: throw LetterException.InvalidLetterAccessException()
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,32 @@
package com.asap.application.letter.service

import com.asap.application.letter.exception.LetterException
import com.asap.application.letter.port.`in`.SendLetterUsecase
import com.asap.application.letter.port.`in`.VerifyLetterAccessibleUsecase
import com.asap.application.letter.port.out.SendLetterManagementPort
import com.asap.application.user.port.out.UserManagementPort
import com.asap.domain.common.DomainId
import com.asap.domain.letter.entity.SendLetter
import com.asap.domain.user.entity.User
import com.asap.domain.user.vo.UserPermission
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.nulls.shouldNotBeNull
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import java.time.LocalDate


class LetterCommandServiceTest:BehaviorSpec({

val mockSendLetterManagementPort = mockk<SendLetterManagementPort>(relaxed=true)
val mockUserManagementPort = mockk<UserManagementPort>(relaxed=true)

val letterCommandService = LetterCommandService(mockSendLetterManagementPort)
val letterCommandService = LetterCommandService(
mockSendLetterManagementPort,
mockUserManagementPort
)



Expand All @@ -36,5 +50,79 @@ class LetterCommandServiceTest:BehaviorSpec({
}
}
}

given("편지 검증 시에"){
val letterCode = "letter-code"
val verifyCommand = VerifyLetterAccessibleUsecase.Command(
letterCode = letterCode,
userId = "user-id"
)
val sendLetter = SendLetter(
receiverName = "receiver-name",
content = "content",
images = emptyList(),
templateType = 1,
senderId = mockk(),
letterCode = letterCode
)
val mockUser = User(
id = DomainId("user-id"),
username = "receiver-name",
profileImage = "profile-image",
permission = UserPermission(true,true,true),
birthday = LocalDate.now()
)
every { mockSendLetterManagementPort.verifiedLetter(any(), any()) } returns false
every { mockSendLetterManagementPort.getLetterByCodeNotNull(any()) } returns sendLetter
every { mockUserManagementPort.getUserNotNull(any()) } returns mockUser
`when`("이전에 열람한 적이 없고, 수신자 이름과 같다면"){
val response = letterCommandService.verify(verifyCommand)
then("편지 코드가 검증되고, 편지 ID가 반환되어야 한다"){
response.letterId shouldNotBeNull {
this.isNotBlank()
this.isNotEmpty()
}
verify { mockSendLetterManagementPort.expireLetter(mockUser.id, sendLetter.id) }
}
}

every { mockSendLetterManagementPort.getLetterByCodeNotNull(any()) } throws LetterException.SendLetterNotFoundException()
`when`("코드가 존재하지 않는다면"){
then("예외가 발생해야 한다"){
shouldThrow<LetterException.SendLetterNotFoundException> {
letterCommandService.verify(verifyCommand)
}
}
}

val anotherUser = User(
id = DomainId("user-id"),
username = "another-name",
profileImage = "profile-image",
permission = UserPermission(true,true,true),
birthday = LocalDate.now()
)
every { mockSendLetterManagementPort.getLetterByCodeNotNull(any()) } returns sendLetter
every { mockUserManagementPort.getUserNotNull(any()) } returns anotherUser
`when`("편지의 수신자 이름과 사용자 이름이 다르면"){
then("예외가 발생해야 한다"){
shouldThrow<LetterException.InvalidLetterAccessException> {
letterCommandService.verify(verifyCommand)
}
}
}

every { mockSendLetterManagementPort.verifiedLetter(any(), any()) } returns true
every { mockSendLetterManagementPort.getExpiredLetterNotNull(any(), any()) } returns sendLetter
`when`("이전에 열람한 적이 있는 사용자라면"){
val response = letterCommandService.verify(verifyCommand)
then("편지 코드가 검증되고, 편지 ID가 반환되어야 한다"){
response.letterId shouldNotBeNull {
this.isNotBlank()
this.isNotEmpty()
}
}
}
}
}) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.asap.application.letter

import com.asap.application.letter.port.out.SendLetterManagementPort
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean

@TestConfiguration
class LetterApplicationConfig(
private val sendLetterManagementPort: SendLetterManagementPort
) {

@Bean
fun letterMockManager(): LetterMockManager {
return LetterMockManager(sendLetterManagementPort)
}
}
Loading
Loading