diff --git a/domain/src/main/kotlin/com/threedays/domain/auth/entity/AuthCode.kt b/domain/src/main/kotlin/com/threedays/domain/auth/entity/AuthCode.kt new file mode 100644 index 0000000..24e4d20 --- /dev/null +++ b/domain/src/main/kotlin/com/threedays/domain/auth/entity/AuthCode.kt @@ -0,0 +1,52 @@ +package com.threedays.domain.auth.entity + +import com.threedays.domain.auth.exception.AuthException +import com.threedays.domain.auth.vo.AuthCodeId +import com.threedays.support.common.base.domain.AggregateRoot +import com.threedays.support.common.base.domain.UUIDTypeId +import java.time.LocalDateTime + +data class AuthCode( + override val id: AuthCodeId, + val code: Code, + val expireAt: LocalDateTime, +) : AggregateRoot() { + + @JvmInline + value class Code(val value: String) { + + init { + require(value.length == 6) { "인증 코드는 6자리 숫자로 구성되어야 합니다." } + require(value.all { it.isDigit() }) { "인증 코드는 숫자로만 구성되어야 합니다." } + } + + companion object { + + fun generate(): Code { + val random: Int = (0..999999).random() + val code: String = random.toString().padStart(6, '0') + return Code(code) + } + + } + } + + companion object { + + fun create(expireAt: LocalDateTime): AuthCode { + return AuthCode(UUIDTypeId.random(), Code.generate(), expireAt) + } + + } + + fun verify(code: Code) { + if (this.code != code) { + throw AuthException.InvalidAuthCodeException() + } + + if (LocalDateTime.now() > expireAt) { + throw AuthException.AuthCodeExpiredException() + } + } + +} diff --git a/domain/src/main/kotlin/com/threedays/domain/auth/exception/AuthException.kt b/domain/src/main/kotlin/com/threedays/domain/auth/exception/AuthException.kt new file mode 100644 index 0000000..a3b0b59 --- /dev/null +++ b/domain/src/main/kotlin/com/threedays/domain/auth/exception/AuthException.kt @@ -0,0 +1,18 @@ +package com.threedays.domain.auth.exception + +import com.threedays.support.common.base.exception.CustomException + +sealed class AuthException( + codeNumber: Int, + override val message: String = DEFAULT_MESSAGE, +) : CustomException("Auth", codeNumber, message) { + + data class AuthCodeExpiredException( + override val message: String = "인증 코드가 만료되었습니다.", + ) : AuthException(1001, message) + + data class InvalidAuthCodeException( + override val message: String = "유효하지 않은 인증 코드입니다.", + ) : AuthException(1002, message) + +} diff --git a/domain/src/main/kotlin/com/threedays/domain/auth/repository/AuthCodeRepository.kt b/domain/src/main/kotlin/com/threedays/domain/auth/repository/AuthCodeRepository.kt new file mode 100644 index 0000000..8f0a9fa --- /dev/null +++ b/domain/src/main/kotlin/com/threedays/domain/auth/repository/AuthCodeRepository.kt @@ -0,0 +1,7 @@ +package com.threedays.domain.auth.repository + +import com.threedays.domain.auth.entity.AuthCode +import com.threedays.domain.auth.vo.AuthCodeId +import com.threedays.support.common.base.domain.Repository + +interface AuthCodeRepository : Repository diff --git a/domain/src/main/kotlin/com/threedays/domain/auth/vo/AuthCodeId.kt b/domain/src/main/kotlin/com/threedays/domain/auth/vo/AuthCodeId.kt new file mode 100644 index 0000000..dd0806d --- /dev/null +++ b/domain/src/main/kotlin/com/threedays/domain/auth/vo/AuthCodeId.kt @@ -0,0 +1,6 @@ +package com.threedays.domain.auth.vo + +import com.threedays.support.common.base.domain.UUIDTypeId +import java.util.* + +data class AuthCodeId(override val value: UUID) : UUIDTypeId(value) diff --git a/domain/src/test/kotlin/com/threedays/domain/auth/entity/AuthCodeTest.kt b/domain/src/test/kotlin/com/threedays/domain/auth/entity/AuthCodeTest.kt new file mode 100644 index 0000000..3c79a02 --- /dev/null +++ b/domain/src/test/kotlin/com/threedays/domain/auth/entity/AuthCodeTest.kt @@ -0,0 +1,117 @@ +package com.threedays.domain.auth.entity + +import com.threedays.domain.auth.exception.AuthException +import io.kotest.assertions.throwables.shouldNotThrow +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.annotation.DisplayName +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import java.time.LocalDateTime + +@DisplayName("[도메인] - 인증 코드") +class AuthCodeTest : DescribeSpec({ + + describe("인증 코드 생성") { + it("새로운 인증 코드를 생성한다") { + // arrange + val expireAt = LocalDateTime.now().plusMinutes(5) + + // act + val authCode = AuthCode.create(expireAt) + + // assert + authCode.id shouldNotBe null + authCode.code.value.length shouldBe 6 + authCode.code.value.all { it.isDigit() } shouldBe true + authCode.expireAt shouldBe expireAt + } + } + + describe("인증 코드 검증") { + context("유효한 인증 코드로") { + it("검증에 성공한다") { + // arrange + val expireAt: LocalDateTime = LocalDateTime.now().plusMinutes(5) + val authCode = AuthCode.create(expireAt) + val validCode = authCode.code + + // act & assert + shouldNotThrow { + authCode.verify(validCode) + } + } + } + + context("잘못된 인증 코드로") { + it("InvalidAuthCodeException을 발생시킨다") { + // arrange + val expireAt = LocalDateTime.now().plusMinutes(5) + val authCode = AuthCode.create(expireAt) + val invalidCode = AuthCode.Code("000000") + + // act & assert + shouldThrow { + authCode.verify(invalidCode) + } + } + } + + context("만료된 인증 코드로") { + it("AuthCodeExpiredException을 발생시킨다") { + // arrange + val expireAt = LocalDateTime.now().minusMinutes(1) + val authCode = AuthCode.create(expireAt) + + // act & assert + shouldThrow { + authCode.verify(authCode.code) + } + } + } + } + + describe("인증 코드 값 객체 (Code)") { + context("유효한 코드로") { + it("Code 객체를 생성한다") { + // act + val code = AuthCode.Code("123456") + + // assert + code.value shouldBe "123456" + } + } + + context("6자리가 아닌 코드로") { + it("예외를 발생시킨다") { + // act & assert + shouldThrow { + AuthCode.Code("12345") + } + shouldThrow { + AuthCode.Code("1234567") + } + } + } + + context("숫자가 아닌 문자가 포함된 코드로") { + it("예외를 발생시킨다") { + // act & assert + shouldThrow { + AuthCode.Code("12345a") + } + } + } + + context("코드 생성 시") { + it("6자리 숫자로 구성된 코드를 생성한다") { + // act + val generatedCode = AuthCode.Code.generate() + + // assert + generatedCode.value.length shouldBe 6 + generatedCode.value.all { it.isDigit() } shouldBe true + } + } + } +}) diff --git a/openapi b/openapi index 7f7f28b..9c4762a 160000 --- a/openapi +++ b/openapi @@ -1 +1 @@ -Subproject commit 7f7f28bcfc288b15b46a9c8d5b1545b4340f3e29 +Subproject commit 9c4762a9b91518c80ee3a5282b097a572aea3176