From d1176acea026d4613e2756364cdd33f570585c57 Mon Sep 17 00:00:00 2001 From: Sim-km Date: Thu, 29 Aug 2024 00:12:06 +0900 Subject: [PATCH 1/8] =?UTF-8?q?ASAP-60=20feat:=20cors=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/asap/bootstrap/common/WebConfig.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/common/WebConfig.kt diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/common/WebConfig.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/common/WebConfig.kt new file mode 100644 index 0000000..944ebb9 --- /dev/null +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/common/WebConfig.kt @@ -0,0 +1,17 @@ +package com.asap.bootstrap.common + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.CorsRegistry +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer + +@Configuration +class WebConfig : WebMvcConfigurer { + + override fun addCorsMappings(registry: CorsRegistry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods("GET", "POST", "PUT", "DELETE") + .allowedHeaders("*") + } + +} \ No newline at end of file From 855cf57f92c1b0fa97555a910d98da61afcc04b4 Mon Sep 17 00:00:00 2001 From: Sim-km Date: Thu, 29 Aug 2024 00:13:05 +0900 Subject: [PATCH 2/8] =?UTF-8?q?ASAP-60=20feat:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bootstrap/common/ExceptionResponse.kt | 16 ++++++++++++++ .../common/GlobalExceptionHandler.kt | 21 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/common/ExceptionResponse.kt create mode 100644 Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/common/GlobalExceptionHandler.kt diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/common/ExceptionResponse.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/common/ExceptionResponse.kt new file mode 100644 index 0000000..5da3bce --- /dev/null +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/common/ExceptionResponse.kt @@ -0,0 +1,16 @@ +package com.asap.bootstrap.common + +import com.asap.common.exception.BusinessException + + +data class ExceptionResponse( + val message: String, + val code: String +) { + + companion object{ + fun of(businessException: BusinessException): ExceptionResponse{ + return ExceptionResponse(businessException.message, businessException.code) + } + } +} \ No newline at end of file diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/common/GlobalExceptionHandler.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/common/GlobalExceptionHandler.kt new file mode 100644 index 0000000..02bf6cf --- /dev/null +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/common/GlobalExceptionHandler.kt @@ -0,0 +1,21 @@ +package com.asap.bootstrap.common + +import com.asap.common.exception.BusinessException +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice + +@RestControllerAdvice +class GlobalExceptionHandler { + + @ExceptionHandler(Exception::class) + fun handleException(e: Exception): ResponseEntity { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.message) + } + + @ExceptionHandler(BusinessException::class) + fun handleBusinessException(e: BusinessException): ResponseEntity { + return ResponseEntity.status(e.httpStatus).body(ExceptionResponse.of(e)) + } +} \ No newline at end of file From ed340829acd5b912cc61b2bfe43514a3cffa01f0 Mon Sep 17 00:00:00 2001 From: Sim-km Date: Thu, 29 Aug 2024 00:15:19 +0900 Subject: [PATCH 3/8] =?UTF-8?q?ASAP-60=20feat:=20User,Auth=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20usecase=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/asap/bootstrap/AppApplication.kt | 13 -- .../auth/controller/AuthController.kt | 22 ++-- .../user/controller/UserController.kt | 16 ++- .../auth/controller/AuthControllerTest.kt | 84 +++++++++++++ .../user/controller/UserControllerTest.kt | 25 ++-- .../auth/AuthApiIntegrationTest.kt} | 36 +++--- .../user/UserApiIntegrationTest.kt | 111 ++++++++++++++++++ 7 files changed, 249 insertions(+), 58 deletions(-) delete mode 100644 Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/AppApplication.kt create mode 100644 Bootstrap-Module/src/test/java/com/asap/bootstrap/acceptance/auth/controller/AuthControllerTest.kt rename Bootstrap-Module/src/test/java/com/asap/bootstrap/{ => acceptance}/user/controller/UserControllerTest.kt (70%) rename Bootstrap-Module/src/test/java/com/asap/bootstrap/{auth/controller/AuthControllerTest.kt => integration/auth/AuthApiIntegrationTest.kt} (75%) create mode 100644 Bootstrap-Module/src/test/java/com/asap/bootstrap/integration/user/UserApiIntegrationTest.kt diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/AppApplication.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/AppApplication.kt deleted file mode 100644 index a358180..0000000 --- a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/AppApplication.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.asap.bootstrap - -import org.springframework.boot.SpringApplication -import org.springframework.boot.autoconfigure.SpringBootApplication - -@SpringBootApplication -class AppApplication { - -} - -fun main(args: Array) { - SpringApplication.run(AppApplication::class.java, *args) -} diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/auth/controller/AuthController.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/auth/controller/AuthController.kt index f39430b..bc34455 100644 --- a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/auth/controller/AuthController.kt +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/auth/controller/AuthController.kt @@ -1,5 +1,6 @@ package com.asap.bootstrap.auth.controller +import com.asap.application.user.port.`in`.SocialLoginUsecase import com.asap.bootstrap.auth.api.AuthApi import com.asap.bootstrap.auth.dto.SocialLoginRequest import com.asap.bootstrap.auth.dto.SocialLoginResponse @@ -9,20 +10,27 @@ import org.springframework.web.bind.annotation.RestController @RestController class AuthController( + private val socialLoginUsecase: SocialLoginUsecase ) : AuthApi { override fun socialLogin( provider: String, request: SocialLoginRequest ): ResponseEntity { - when (request.accessToken) { - "nonRegistered" -> return ResponseEntity.status(HttpStatus.UNAUTHORIZED) - .body(SocialLoginResponse.NonRegistered("registerToken")) + val command = SocialLoginUsecase.Command( + provider = provider, + accessToken = request.accessToken + ) + return when (val response = socialLoginUsecase.login(command)) { + is SocialLoginUsecase.Success -> ResponseEntity.ok( + SocialLoginResponse.Success( + response.accessToken, + response.refreshToken + ) + ) - "registered" -> return ResponseEntity - .ok(SocialLoginResponse.Success("accessToken", "refreshToken")) - - else -> return ResponseEntity.badRequest().build() + is SocialLoginUsecase.NonRegistered -> ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(SocialLoginResponse.NonRegistered(response.registerToken)) } } } \ No newline at end of file diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/user/controller/UserController.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/user/controller/UserController.kt index 9be896e..067e899 100644 --- a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/user/controller/UserController.kt +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/user/controller/UserController.kt @@ -1,5 +1,6 @@ package com.asap.bootstrap.user.controller +import com.asap.application.user.port.`in`.RegisterUserUsecase import com.asap.bootstrap.user.api.UserApi import com.asap.bootstrap.user.dto.RegisterUserRequest import com.asap.bootstrap.user.dto.RegisterUserResponse @@ -8,13 +9,18 @@ import org.springframework.web.bind.annotation.RestController @RestController class UserController( - + private val registerUserUsecase: RegisterUserUsecase, ) : UserApi{ override fun registerUser(request: RegisterUserRequest): ResponseEntity { - when(request.registerToken){ - "register" -> return ResponseEntity.ok(RegisterUserResponse("accessToken", "refreshToken")) - else -> return ResponseEntity.badRequest().build() - } + val response = registerUserUsecase.registerUser( + RegisterUserUsecase.Command( + request.registerToken, + request.servicePermission, + request.privatePermission, + request.marketingPermission, + request.birthday + )) + return ResponseEntity.ok(RegisterUserResponse(response.accessToken, response.refreshToken)) } } \ No newline at end of file diff --git a/Bootstrap-Module/src/test/java/com/asap/bootstrap/acceptance/auth/controller/AuthControllerTest.kt b/Bootstrap-Module/src/test/java/com/asap/bootstrap/acceptance/auth/controller/AuthControllerTest.kt new file mode 100644 index 0000000..75e8ee6 --- /dev/null +++ b/Bootstrap-Module/src/test/java/com/asap/bootstrap/acceptance/auth/controller/AuthControllerTest.kt @@ -0,0 +1,84 @@ +package com.asap.bootstrap.acceptance.auth.controller + +import com.asap.application.user.port.`in`.SocialLoginUsecase +import com.asap.bootstrap.auth.controller.AuthController +import com.asap.bootstrap.auth.dto.SocialLoginRequest +import com.fasterxml.jackson.databind.ObjectMapper +import org.junit.jupiter.api.Test +import org.mockito.BDDMockito +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.post + +@WebMvcTest(AuthController::class) +@AutoConfigureMockMvc +class AuthControllerTest { + + @Autowired + private lateinit var mockMvc: MockMvc + + @MockBean + private lateinit var socialLoginUsecase: SocialLoginUsecase + + private val objectMapper: ObjectMapper = ObjectMapper() + + + @Test + fun socialLoginSuccessTest() { + // given + val request = SocialLoginRequest("registered") + val command = SocialLoginUsecase.Command("KAKAO","registered") + BDDMockito.given(socialLoginUsecase.login(command)) + .willReturn(SocialLoginUsecase.Success("accessToken", "refreshToken")) + // when + val response = mockMvc.post("/api/v1/auth/login/{provider}", "KAKAO") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(request) + } + + // then + response.andExpect { + status { isOk() } + jsonPath("$.accessToken") { + exists() + isString() + isNotEmpty() + } + jsonPath("$.refreshToken") { + exists() + isString() + isNotEmpty() + } + } + } + + @Test + fun socialLoginNonRegisteredTest() { + // given + val request = SocialLoginRequest("nonRegistered") + val command = SocialLoginUsecase.Command("KAKAO","nonRegistered") + BDDMockito.given(socialLoginUsecase.login(command)) + .willReturn(SocialLoginUsecase.NonRegistered("registerToken")) + // when + val response = mockMvc.post("/api/v1/auth/login/{provider}", "KAKAO") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(request) + } + + // then + response.andExpect { + status { isUnauthorized() } + jsonPath("$.registerToken") { + exists() + isString() + isNotEmpty() + } + } + } + + +} \ No newline at end of file diff --git a/Bootstrap-Module/src/test/java/com/asap/bootstrap/user/controller/UserControllerTest.kt b/Bootstrap-Module/src/test/java/com/asap/bootstrap/acceptance/user/controller/UserControllerTest.kt similarity index 70% rename from Bootstrap-Module/src/test/java/com/asap/bootstrap/user/controller/UserControllerTest.kt rename to Bootstrap-Module/src/test/java/com/asap/bootstrap/acceptance/user/controller/UserControllerTest.kt index d754270..88340cb 100644 --- a/Bootstrap-Module/src/test/java/com/asap/bootstrap/user/controller/UserControllerTest.kt +++ b/Bootstrap-Module/src/test/java/com/asap/bootstrap/acceptance/user/controller/UserControllerTest.kt @@ -1,12 +1,16 @@ -package com.asap.bootstrap.user.controller +package com.asap.bootstrap.acceptance.user.controller +import com.asap.application.user.port.`in`.RegisterUserUsecase +import com.asap.bootstrap.user.controller.UserController import com.asap.bootstrap.user.dto.RegisterUserRequest import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import org.junit.jupiter.api.Test +import org.mockito.BDDMockito.given import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.post @@ -19,12 +23,17 @@ class UserControllerTest { @Autowired private lateinit var mockMvc: MockMvc + @MockBean + private lateinit var registerUserUsecase: RegisterUserUsecase + private val objectMapper: ObjectMapper = ObjectMapper().registerModules(JavaTimeModule()) @Test fun registerUserTest(){ // given val request = RegisterUserRequest("register", true, true, true, LocalDate.now()) + val command = RegisterUserUsecase.Command(request.registerToken, request.servicePermission, request.privatePermission, request.marketingPermission, request.birthday) + given(registerUserUsecase.registerUser(command)).willReturn(RegisterUserUsecase.Response("accessToken", "refreshToken")) // when val response = mockMvc.post("/api/v1/users") { contentType = MediaType.APPLICATION_JSON @@ -46,18 +55,4 @@ class UserControllerTest { } } - @Test - fun registerUserNotExistsRegisterTokenTest(){ - // given - val request = RegisterUserRequest("nonExistsToken", false, true, true, LocalDate.now()) - // when - val response = mockMvc.post("/api/v1/users") { - contentType = MediaType.APPLICATION_JSON - content = objectMapper.writeValueAsString(request) - } - // then - response.andExpect { - status { isBadRequest() } - } - } } \ No newline at end of file diff --git a/Bootstrap-Module/src/test/java/com/asap/bootstrap/auth/controller/AuthControllerTest.kt b/Bootstrap-Module/src/test/java/com/asap/bootstrap/integration/auth/AuthApiIntegrationTest.kt similarity index 75% rename from Bootstrap-Module/src/test/java/com/asap/bootstrap/auth/controller/AuthControllerTest.kt rename to Bootstrap-Module/src/test/java/com/asap/bootstrap/integration/auth/AuthApiIntegrationTest.kt index 04d4195..082a92a 100644 --- a/Bootstrap-Module/src/test/java/com/asap/bootstrap/auth/controller/AuthControllerTest.kt +++ b/Bootstrap-Module/src/test/java/com/asap/bootstrap/integration/auth/AuthApiIntegrationTest.kt @@ -1,35 +1,37 @@ -package com.asap.bootstrap.auth.controller +package com.asap.bootstrap.integration.auth import com.asap.bootstrap.auth.dto.SocialLoginRequest import com.fasterxml.jackson.databind.ObjectMapper -import org.junit.jupiter.api.Test +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.boot.test.context.SpringBootTest import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.post +import kotlin.test.Test -@WebMvcTest(AuthController::class) + +@SpringBootTest @AutoConfigureMockMvc -class AuthControllerTest { +class AuthApiIntegrationTest { @Autowired - private lateinit var mockMvc: MockMvc + lateinit var mockMvc: MockMvc - private val objectMapper: ObjectMapper = ObjectMapper() + val objectMapper: ObjectMapper = ObjectMapper().registerModules(JavaTimeModule()) @Test - fun socialLoginSuccessTest(){ + fun socialLoginSuccessTest() { // given val request = SocialLoginRequest("registered") + val provider = "KAKAO" // when - val response = mockMvc.post("/api/v1/auth/login/{provider}", "kakao") { + val response = mockMvc.post("/api/v1/auth/login/{provider}", provider) { contentType = MediaType.APPLICATION_JSON content = objectMapper.writeValueAsString(request) } - // then response.andExpect { status { isOk() } @@ -47,15 +49,15 @@ class AuthControllerTest { } @Test - fun socialLoginNonRegisteredTest(){ + fun socialLoginNonRegisteredTest() { // given val request = SocialLoginRequest("nonRegistered") + val provider = "KAKAO" // when - val response = mockMvc.post("/api/v1/auth/login/{provider}", "kakao") { + val response = mockMvc.post("/api/v1/auth/login/{provider}", provider) { contentType = MediaType.APPLICATION_JSON content = objectMapper.writeValueAsString(request) } - // then response.andExpect { status { isUnauthorized() } @@ -67,22 +69,20 @@ class AuthControllerTest { } } - @Test - fun socialLoginBadRequestTest(){ + fun socialLoginBadRequestTest_with_invalid_access_token() { // given val request = SocialLoginRequest("invalid") + val provider = "KAKAO" // when - val response = mockMvc.post("/api/v1/auth/login/{provider}", "kakao") { + val response = mockMvc.post("/api/v1/auth/login/{provider}", provider) { contentType = MediaType.APPLICATION_JSON content = objectMapper.writeValueAsString(request) } - // then response.andExpect { status { isBadRequest() } } } - } \ No newline at end of file diff --git a/Bootstrap-Module/src/test/java/com/asap/bootstrap/integration/user/UserApiIntegrationTest.kt b/Bootstrap-Module/src/test/java/com/asap/bootstrap/integration/user/UserApiIntegrationTest.kt new file mode 100644 index 0000000..d72e7e1 --- /dev/null +++ b/Bootstrap-Module/src/test/java/com/asap/bootstrap/integration/user/UserApiIntegrationTest.kt @@ -0,0 +1,111 @@ +package com.asap.bootstrap.integration.user + +import com.asap.bootstrap.user.dto.RegisterUserRequest +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.post +import java.time.LocalDate + +/** + * TODO: 요청 생성 util 클래스 추가하기 + */ +@SpringBootTest +@AutoConfigureMockMvc +class UserApiIntegrationTest { + + @Autowired + lateinit var mockMvc: MockMvc + + val objectMapper: ObjectMapper = ObjectMapper().registerModules(JavaTimeModule()) + + @Test + fun registerUserSuccessTest() { + // given + val request = RegisterUserRequest("valid", true, true, true, LocalDate.now()) + // when + val response = mockMvc.post("/api/v1/users") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(request) + } + // then + response.andExpect { + status { isOk() } + jsonPath("$.accessToken") { + exists() + isString() + isNotEmpty() + } + jsonPath("$.refreshToken") { + exists() + isString() + isNotEmpty() + } + } + } + + @Test + fun registerUserInvalidTest_with_DuplicateUser(){ + // given + val request = RegisterUserRequest("duplicate", true, true, true, LocalDate.now()) + // when + val response = mockMvc.post("/api/v1/users") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(request) + } + // then + response.andExpect { + status { isBadRequest() } + } + } + + @Test + fun registerUserInvalidTest_with_InvalidRegisterToken(){ + // given + val request = RegisterUserRequest("invalid", true, true, true, LocalDate.now()) + // when + val response = mockMvc.post("/api/v1/users") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(request) + } + // then + response.andExpect { + status { isBadRequest() } + } + } + + @Test + fun registerUserInvalidTest_with_InvalidServicePermission(){ + //given + val request = RegisterUserRequest("valid", false, true, true, LocalDate.now()) + //when + val response = mockMvc.post("/api/v1/users") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(request) + } + //then + response.andExpect { + status { isBadRequest() } + } + } + + @Test + fun registerUserInvalidTest_with_InvalidPrivatePermission(){ + //given + val request = RegisterUserRequest("valid", true, false, true, LocalDate.now()) + //when + val response = mockMvc.post("/api/v1/users") { + contentType = MediaType.APPLICATION_JSON + content = objectMapper.writeValueAsString(request) + } + //then + response.andExpect { + status { isBadRequest() } + } + } +} \ No newline at end of file From 14b6f7471918031b18faf2b435835464bf8863c5 Mon Sep 17 00:00:00 2001 From: Sim-km Date: Thu, 29 Aug 2024 00:18:19 +0900 Subject: [PATCH 4/8] =?UTF-8?q?ASAP-60=20feat:=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EB=AA=A8=EB=93=88=20=EC=A0=95=EC=9D=98(=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Domain-Module/build.gradle.kts | 5 ++++ .../kotlin/com/asap/domain/common/DomainId.kt | 13 +++++++++++ .../com/asap/domain/user/entity/User.kt | 11 +++++++++ .../com/asap/domain/user/entity/UserAuth.kt | 12 ++++++++++ .../domain/user/enums/SocialLoginProvider.kt | 18 +++++++++++++++ .../com/asap/domain/user/vo/UserPermission.kt | 23 +++++++++++++++++++ 6 files changed, 82 insertions(+) create mode 100644 Domain-Module/build.gradle.kts create mode 100644 Domain-Module/src/main/kotlin/com/asap/domain/common/DomainId.kt create mode 100644 Domain-Module/src/main/kotlin/com/asap/domain/user/entity/User.kt create mode 100644 Domain-Module/src/main/kotlin/com/asap/domain/user/entity/UserAuth.kt create mode 100644 Domain-Module/src/main/kotlin/com/asap/domain/user/enums/SocialLoginProvider.kt create mode 100644 Domain-Module/src/main/kotlin/com/asap/domain/user/vo/UserPermission.kt diff --git a/Domain-Module/build.gradle.kts b/Domain-Module/build.gradle.kts new file mode 100644 index 0000000..3c88491 --- /dev/null +++ b/Domain-Module/build.gradle.kts @@ -0,0 +1,5 @@ +dependencies{ + implementation("com.fasterxml.uuid:java-uuid-generator:5.1.0") + + implementation(project(":Common-Module")) +} \ No newline at end of file diff --git a/Domain-Module/src/main/kotlin/com/asap/domain/common/DomainId.kt b/Domain-Module/src/main/kotlin/com/asap/domain/common/DomainId.kt new file mode 100644 index 0000000..f298782 --- /dev/null +++ b/Domain-Module/src/main/kotlin/com/asap/domain/common/DomainId.kt @@ -0,0 +1,13 @@ +package com.asap.domain.common + +import com.fasterxml.uuid.Generators + +data class DomainId( + val id: String +) { + companion object{ + fun generate(): DomainId { + return DomainId(Generators.timeBasedEpochGenerator().generate().toString()) + } + } +} \ No newline at end of file diff --git a/Domain-Module/src/main/kotlin/com/asap/domain/user/entity/User.kt b/Domain-Module/src/main/kotlin/com/asap/domain/user/entity/User.kt new file mode 100644 index 0000000..4bbc9cc --- /dev/null +++ b/Domain-Module/src/main/kotlin/com/asap/domain/user/entity/User.kt @@ -0,0 +1,11 @@ +package com.asap.domain.user.entity + +import com.asap.domain.common.DomainId +import com.asap.domain.user.vo.UserPermission + +data class User( + val id: DomainId = DomainId.generate(), + val nickname: String, + val permission: UserPermission +) { +} \ No newline at end of file diff --git a/Domain-Module/src/main/kotlin/com/asap/domain/user/entity/UserAuth.kt b/Domain-Module/src/main/kotlin/com/asap/domain/user/entity/UserAuth.kt new file mode 100644 index 0000000..aa5d0cd --- /dev/null +++ b/Domain-Module/src/main/kotlin/com/asap/domain/user/entity/UserAuth.kt @@ -0,0 +1,12 @@ +package com.asap.domain.user.entity + +import com.asap.domain.common.DomainId +import com.asap.domain.user.enums.SocialLoginProvider + +data class UserAuth( + val id: DomainId = DomainId.generate(), + val userId: DomainId, + val socialId: String, + val socialLoginProvider: SocialLoginProvider +) { +} \ No newline at end of file diff --git a/Domain-Module/src/main/kotlin/com/asap/domain/user/enums/SocialLoginProvider.kt b/Domain-Module/src/main/kotlin/com/asap/domain/user/enums/SocialLoginProvider.kt new file mode 100644 index 0000000..8de56f2 --- /dev/null +++ b/Domain-Module/src/main/kotlin/com/asap/domain/user/enums/SocialLoginProvider.kt @@ -0,0 +1,18 @@ +package com.asap.domain.user.enums + +import com.asap.common.exception.DefaultException + +enum class SocialLoginProvider { + KAKAO; + + companion object{ + fun parse(value: String): SocialLoginProvider { + return when (value) { + entries.firstOrNull { it.name == value }?.name -> valueOf(value) + else -> throw DefaultException.InvalidArgumentException("유효하지 않은 소셜 로그인 제공자입니다.") + } + } + } + + +} \ No newline at end of file diff --git a/Domain-Module/src/main/kotlin/com/asap/domain/user/vo/UserPermission.kt b/Domain-Module/src/main/kotlin/com/asap/domain/user/vo/UserPermission.kt new file mode 100644 index 0000000..3174177 --- /dev/null +++ b/Domain-Module/src/main/kotlin/com/asap/domain/user/vo/UserPermission.kt @@ -0,0 +1,23 @@ +package com.asap.domain.user.vo + +import com.asap.common.exception.DefaultException + +data class UserPermission( + val servicePermission: Boolean, + val privatePermission: Boolean, + val marketingPermission: Boolean +) { + + init{ + validate() + } + + private fun validate(){ + if(!servicePermission){ + throw DefaultException.InvalidDefaultException("서비스 이용약관에 동의해야 합니다.") + } + if(!privatePermission){ + throw DefaultException.InvalidDefaultException("개인정보 수집 및 이용에 동의해야 합니다.") + } + } +} \ No newline at end of file From 2c0817696f2375345ead1be825864553041de82f Mon Sep 17 00:00:00 2001 From: Sim-km Date: Thu, 29 Aug 2024 00:20:19 +0900 Subject: [PATCH 5/8] =?UTF-8?q?ASAP-60=20feat:=20=EC=95=A0=ED=94=8C?= =?UTF-8?q?=EB=A6=AC=EC=BC=80=EC=9D=B4=EC=85=98=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Usecase: 소셜 로그인, 회원가입 - usecase 구현 - service - output 임시 구현체(Memory-Adapter) --- Application-Module/README.md | 27 +++++ Application-Module/build.gradle.kts | 4 + .../com/asap/application/ApplicationConfig.kt | 10 ++ .../user/exception/UserException.kt | 33 ++++++ .../user/port/in/RegisterUserUsecase.kt | 22 ++++ .../user/port/in/SocialLoginUsecase.kt | 22 ++++ .../user/port/out/AuthInfoRetrievePort.kt | 11 ++ .../user/port/out/UserAuthManagementPort.kt | 24 ++++ .../user/port/out/UserManagementPort.kt | 10 ++ .../user/port/out/UserTokenManagementPort.kt | 18 +++ .../memory/MemoryAuthInfoRetrieveAdapter.kt | 22 ++++ .../out/memory/MemoryAuthManagementAdapter.kt | 30 +++++ .../out/memory/MemoryUserManagementAdapter.kt | 25 +++++ .../MemoryUserTokenManagementAdapter.kt | 43 ++++++++ .../user/service/RegisterUserService.kt | 54 +++++++++ .../user/service/SocialLoginService.kt | 44 ++++++++ .../com/asap/application/user/vo/AuthInfo.kt | 10 ++ .../asap/application/user/vo/UserClaims.kt | 13 +++ .../user/service/RegisterUserServiceTest.kt | 103 ++++++++++++++++++ .../user/service/SocialLoginServiceTest.kt | 90 +++++++++++++++ 20 files changed, 615 insertions(+) create mode 100644 Application-Module/README.md create mode 100644 Application-Module/build.gradle.kts create mode 100644 Application-Module/src/main/kotlin/com/asap/application/ApplicationConfig.kt create mode 100644 Application-Module/src/main/kotlin/com/asap/application/user/exception/UserException.kt create mode 100644 Application-Module/src/main/kotlin/com/asap/application/user/port/in/RegisterUserUsecase.kt create mode 100644 Application-Module/src/main/kotlin/com/asap/application/user/port/in/SocialLoginUsecase.kt create mode 100644 Application-Module/src/main/kotlin/com/asap/application/user/port/out/AuthInfoRetrievePort.kt create mode 100644 Application-Module/src/main/kotlin/com/asap/application/user/port/out/UserAuthManagementPort.kt create mode 100644 Application-Module/src/main/kotlin/com/asap/application/user/port/out/UserManagementPort.kt create mode 100644 Application-Module/src/main/kotlin/com/asap/application/user/port/out/UserTokenManagementPort.kt create mode 100644 Application-Module/src/main/kotlin/com/asap/application/user/port/out/memory/MemoryAuthInfoRetrieveAdapter.kt create mode 100644 Application-Module/src/main/kotlin/com/asap/application/user/port/out/memory/MemoryAuthManagementAdapter.kt create mode 100644 Application-Module/src/main/kotlin/com/asap/application/user/port/out/memory/MemoryUserManagementAdapter.kt create mode 100644 Application-Module/src/main/kotlin/com/asap/application/user/port/out/memory/MemoryUserTokenManagementAdapter.kt create mode 100644 Application-Module/src/main/kotlin/com/asap/application/user/service/RegisterUserService.kt create mode 100644 Application-Module/src/main/kotlin/com/asap/application/user/service/SocialLoginService.kt create mode 100644 Application-Module/src/main/kotlin/com/asap/application/user/vo/AuthInfo.kt create mode 100644 Application-Module/src/main/kotlin/com/asap/application/user/vo/UserClaims.kt create mode 100644 Application-Module/src/test/kotlin/com/asap/application/user/service/RegisterUserServiceTest.kt create mode 100644 Application-Module/src/test/kotlin/com/asap/application/user/service/SocialLoginServiceTest.kt diff --git a/Application-Module/README.md b/Application-Module/README.md new file mode 100644 index 0000000..ffca696 --- /dev/null +++ b/Application-Module/README.md @@ -0,0 +1,27 @@ +# Application 모듈 + +## 역할 + +* `Lettering` 서비스의 핵심 비즈니스 로직을 처리한다. + + +## 패키지 구조 + +```markdown +. +└── {domain}/ + ├── port/ + │ ├── in + │ └── out + ├── service + ├── vo + └── exception +``` + +* `{domain}`: 도메인 이름을 의미합니다. 예를 들어, `user` 등이 될 수 있습니다. +* `port`: 외부와의 통신을 위한 인터페이스를 정의합니다. + * `in`: 외부에서 들어오는 요청을 처리하는 인터페이스를 정의합니다. + * `out`: 외부로 나가는 응답을 처리하는 인터페이스를 정의합니다. +* `service`: 비즈니스 로직을 처리하는 구현체입니다. +* `exception`: 비즈니스 로직에서 발생하는 예외를 정의합니다. +* `vo`: 비즈니스 로직에서 사용되는 값 객체를 정의합니다. \ No newline at end of file diff --git a/Application-Module/build.gradle.kts b/Application-Module/build.gradle.kts new file mode 100644 index 0000000..7d3eb07 --- /dev/null +++ b/Application-Module/build.gradle.kts @@ -0,0 +1,4 @@ +dependencies{ + implementation(project(":Domain-Module")) + implementation(project(":Common-Module")) +} \ No newline at end of file diff --git a/Application-Module/src/main/kotlin/com/asap/application/ApplicationConfig.kt b/Application-Module/src/main/kotlin/com/asap/application/ApplicationConfig.kt new file mode 100644 index 0000000..918e40a --- /dev/null +++ b/Application-Module/src/main/kotlin/com/asap/application/ApplicationConfig.kt @@ -0,0 +1,10 @@ +package com.asap.application + +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration + + +@Configuration +@ComponentScan("com.asap.application") +class ApplicationConfig { +} \ No newline at end of file diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/exception/UserException.kt b/Application-Module/src/main/kotlin/com/asap/application/user/exception/UserException.kt new file mode 100644 index 0000000..6fa4537 --- /dev/null +++ b/Application-Module/src/main/kotlin/com/asap/application/user/exception/UserException.kt @@ -0,0 +1,33 @@ +package com.asap.application.user.exception + +import com.asap.common.exception.BusinessException + +sealed class UserException( + codePrefix: String = CODE_PREFIX, + errorCode: Int, + httpStatus: Int = 400, + message: String = DEFAULT_ERROR_MESSAGE +): BusinessException(codePrefix, errorCode, httpStatus, message) { + + class UserAlreadyRegisteredException: UserException( + errorCode = 1, + message = "이미 가입된 사용자입니다." + ) + + + class UserAuthNotFoundException: UserException( + errorCode = 2, + message = "사용자 인증 정보를 찾을 수 없습니다." + ) + + class UserNotFoundException: UserException( + errorCode = 3, + message = "사용자를 찾을 수 없습니다." + ) + + + companion object{ + const val CODE_PREFIX = "USER" + const val DEFAULT_ERROR_MESSAGE = "사용자와 관련된 예외가 발생했습니다." + } +} \ No newline at end of file diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/port/in/RegisterUserUsecase.kt b/Application-Module/src/main/kotlin/com/asap/application/user/port/in/RegisterUserUsecase.kt new file mode 100644 index 0000000..5a02e5d --- /dev/null +++ b/Application-Module/src/main/kotlin/com/asap/application/user/port/in/RegisterUserUsecase.kt @@ -0,0 +1,22 @@ +package com.asap.application.user.port.`in` + +import java.time.LocalDate + +interface RegisterUserUsecase { + + fun registerUser(command: Command): Response + + + data class Command( + val registerToken: String, + val servicePermission: Boolean, + val privatePermission: Boolean, + val marketingPermission: Boolean, + val birthday: LocalDate? + ) + + data class Response( + val accessToken: String, + val refreshToken: String + ) +} \ No newline at end of file diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/port/in/SocialLoginUsecase.kt b/Application-Module/src/main/kotlin/com/asap/application/user/port/in/SocialLoginUsecase.kt new file mode 100644 index 0000000..ecc65ee --- /dev/null +++ b/Application-Module/src/main/kotlin/com/asap/application/user/port/in/SocialLoginUsecase.kt @@ -0,0 +1,22 @@ +package com.asap.application.user.port.`in` + +interface SocialLoginUsecase { + + fun login(command: Command): Response + + data class Command( + val provider: String, + val accessToken: String, + ) + + sealed class Response { + } + data class Success( + val accessToken: String, + val refreshToken: String + ) : Response() + + data class NonRegistered( + val registerToken: String + ) : Response() +} \ No newline at end of file diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/port/out/AuthInfoRetrievePort.kt b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/AuthInfoRetrievePort.kt new file mode 100644 index 0000000..9825553 --- /dev/null +++ b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/AuthInfoRetrievePort.kt @@ -0,0 +1,11 @@ +package com.asap.application.user.port.out + +import com.asap.application.user.exception.UserException +import com.asap.application.user.vo.AuthInfo +import com.asap.domain.user.enums.SocialLoginProvider + +interface AuthInfoRetrievePort { + + @Throws(UserException.UserAuthNotFoundException::class) + fun getAuthInfo(provider: SocialLoginProvider, accessToken: String): AuthInfo +} \ No newline at end of file diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/port/out/UserAuthManagementPort.kt b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/UserAuthManagementPort.kt new file mode 100644 index 0000000..4cee2d3 --- /dev/null +++ b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/UserAuthManagementPort.kt @@ -0,0 +1,24 @@ +package com.asap.application.user.port.out + +import com.asap.domain.user.entity.UserAuth +import com.asap.domain.user.enums.SocialLoginProvider + +interface UserAuthManagementPort { + + fun getUserAuth( + socialId: String, + socialLoginProvider: SocialLoginProvider + ): UserAuth? + + + fun isExistsUserAuth( + socialId: String, + socialLoginProvider: SocialLoginProvider + ): Boolean + + + fun saveUserAuth( + userAuth: UserAuth + ): UserAuth + +} \ No newline at end of file diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/port/out/UserManagementPort.kt b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/UserManagementPort.kt new file mode 100644 index 0000000..c0f876a --- /dev/null +++ b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/UserManagementPort.kt @@ -0,0 +1,10 @@ +package com.asap.application.user.port.out + +import com.asap.domain.common.DomainId +import com.asap.domain.user.entity.User + +interface UserManagementPort { + fun saveUser(user: User): User + + fun getUser(userId: DomainId): User? +} \ No newline at end of file diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/port/out/UserTokenManagementPort.kt b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/UserTokenManagementPort.kt new file mode 100644 index 0000000..f32c57f --- /dev/null +++ b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/UserTokenManagementPort.kt @@ -0,0 +1,18 @@ +package com.asap.application.user.port.out + +import com.asap.application.user.vo.UserClaims +import com.asap.domain.user.entity.User + +interface UserTokenManagementPort { + fun resolveRegisterToken(token: String): UserClaims.Register + + fun generateRegisterToken( + socialId: String, + socialLoginProvider: String, + username: String + ): String + + fun generateAccessToken(user: User): String + + fun generateRefreshToken(user: User): String +} \ No newline at end of file diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/port/out/memory/MemoryAuthInfoRetrieveAdapter.kt b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/memory/MemoryAuthInfoRetrieveAdapter.kt new file mode 100644 index 0000000..205c1b9 --- /dev/null +++ b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/memory/MemoryAuthInfoRetrieveAdapter.kt @@ -0,0 +1,22 @@ +package com.asap.application.user.port.out.memory + +import com.asap.application.user.exception.UserException +import com.asap.application.user.port.out.AuthInfoRetrievePort +import com.asap.application.user.vo.AuthInfo +import com.asap.domain.user.enums.SocialLoginProvider +import org.springframework.context.annotation.Primary +import org.springframework.stereotype.Component + +@Component +@Primary +class MemoryAuthInfoRetrieveAdapter: AuthInfoRetrievePort { + + private val authInfos = mutableMapOf, AuthInfo>().apply { + put(Pair("registered", SocialLoginProvider.KAKAO), AuthInfo(SocialLoginProvider.KAKAO, "socialId", "username")) + put(Pair("nonRegistered", SocialLoginProvider.KAKAO), AuthInfo(SocialLoginProvider.KAKAO, "nonRegisteredId", "username")) + } + + override fun getAuthInfo(provider: SocialLoginProvider, accessToken: String): AuthInfo { + return authInfos[Pair(accessToken, provider)] ?: throw UserException.UserAuthNotFoundException() + } +} \ No newline at end of file diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/port/out/memory/MemoryAuthManagementAdapter.kt b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/memory/MemoryAuthManagementAdapter.kt new file mode 100644 index 0000000..d760c43 --- /dev/null +++ b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/memory/MemoryAuthManagementAdapter.kt @@ -0,0 +1,30 @@ +package com.asap.application.user.port.out.memory + +import com.asap.application.user.port.out.UserAuthManagementPort +import com.asap.domain.common.DomainId +import com.asap.domain.user.entity.UserAuth +import com.asap.domain.user.enums.SocialLoginProvider +import org.springframework.context.annotation.Primary +import org.springframework.stereotype.Component + +@Component +@Primary +class MemoryAuthManagementAdapter: UserAuthManagementPort{ + + private val userAuths = mutableMapOf, UserAuth>().apply { + put(Pair("socialId", SocialLoginProvider.KAKAO), UserAuth(userId = DomainId("registered"), socialId = "socialId", socialLoginProvider = SocialLoginProvider.KAKAO)) + } + + override fun getUserAuth(socialId: String, socialLoginProvider: SocialLoginProvider): UserAuth? { + return userAuths[Pair(socialId, socialLoginProvider)] + } + + override fun isExistsUserAuth(socialId: String, socialLoginProvider: SocialLoginProvider): Boolean { + return socialId == "duplicate" + } + + override fun saveUserAuth(userAuth: UserAuth): UserAuth { + userAuths[Pair(userAuth.socialId, userAuth.socialLoginProvider)] = userAuth + return userAuth + } +} \ No newline at end of file diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/port/out/memory/MemoryUserManagementAdapter.kt b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/memory/MemoryUserManagementAdapter.kt new file mode 100644 index 0000000..e1f55df --- /dev/null +++ b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/memory/MemoryUserManagementAdapter.kt @@ -0,0 +1,25 @@ +package com.asap.application.user.port.out.memory + +import com.asap.application.user.port.out.UserManagementPort +import com.asap.domain.common.DomainId +import com.asap.domain.user.entity.User +import com.asap.domain.user.vo.UserPermission +import org.springframework.context.annotation.Primary +import org.springframework.stereotype.Component + +@Component +@Primary +class MemoryUserManagementAdapter: UserManagementPort { + private val users = mutableMapOf().apply { + put(DomainId("registered"), User(DomainId("registered"), "username", UserPermission(true,true,true))) + } + + override fun saveUser(user: User): User { + users[user.id] = user + return user + } + + override fun getUser(userId: DomainId): User? { + return users[userId] + } +} \ No newline at end of file diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/port/out/memory/MemoryUserTokenManagementAdapter.kt b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/memory/MemoryUserTokenManagementAdapter.kt new file mode 100644 index 0000000..0bfde7c --- /dev/null +++ b/Application-Module/src/main/kotlin/com/asap/application/user/port/out/memory/MemoryUserTokenManagementAdapter.kt @@ -0,0 +1,43 @@ +package com.asap.application.user.port.out.memory + +import com.asap.application.user.port.out.UserTokenManagementPort +import com.asap.application.user.vo.UserClaims +import com.asap.common.exception.DefaultException +import com.asap.domain.user.entity.User +import com.asap.domain.user.enums.SocialLoginProvider +import org.springframework.context.annotation.Primary +import org.springframework.stereotype.Component + +@Component +@Primary +class MemoryUserTokenManagementAdapter( + +) : UserTokenManagementPort{ + override fun resolveRegisterToken(token: String): UserClaims.Register { + return when(token){ + "valid" -> UserClaims.Register( + socialId = "123", + socialLoginProvider = SocialLoginProvider.KAKAO, + username = "test" + ) + "duplicate" -> UserClaims.Register( + socialId = "duplicate", + socialLoginProvider = SocialLoginProvider.KAKAO, + username = "test" + ) + else -> throw DefaultException.InvalidArgumentException() // TODO: jwt 구현할 때 수정 + } + } + + override fun generateRegisterToken(socialId: String, socialLoginProvider: String, username: String): String { + return "registerToken" + } + + override fun generateAccessToken(user: User): String { + return "accessToken" + } + + override fun generateRefreshToken(user: User): String { + return "refreshToken" + } +} \ No newline at end of file diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/service/RegisterUserService.kt b/Application-Module/src/main/kotlin/com/asap/application/user/service/RegisterUserService.kt new file mode 100644 index 0000000..350c8c7 --- /dev/null +++ b/Application-Module/src/main/kotlin/com/asap/application/user/service/RegisterUserService.kt @@ -0,0 +1,54 @@ +package com.asap.application.user.service + +import com.asap.application.user.exception.UserException +import com.asap.application.user.port.`in`.RegisterUserUsecase +import com.asap.application.user.port.out.UserAuthManagementPort +import com.asap.application.user.port.out.UserManagementPort +import com.asap.application.user.port.out.UserTokenManagementPort +import com.asap.domain.user.entity.User +import com.asap.domain.user.entity.UserAuth +import com.asap.domain.user.vo.UserPermission +import org.springframework.stereotype.Service + +@Service +class RegisterUserService( + private val userTokenManagementPort: UserTokenManagementPort, + private val userAuthManagementPort: UserAuthManagementPort, + private val userManagementPort: UserManagementPort +) : RegisterUserUsecase { + + /** + * 1. register token으로부터 사용자 정보 추출 -> 토큰이 이미 사용됐으면 에러 + * 2. 추출한 사용자가 이미 존재하는지 확인 -> 이미 존재하면 에러 + * 3. 추출한 사용자 정보와 함께 사용자 동의 검증 -> 동의하지 않으면 에러 + * 4. 사용자 정보 저장 및 jwt 토큰 반환 + */ + + override fun registerUser(command: RegisterUserUsecase.Command): RegisterUserUsecase.Response { + val userClaims = userTokenManagementPort.resolveRegisterToken(command.registerToken) + if (userAuthManagementPort.isExistsUserAuth(userClaims.socialId, userClaims.socialLoginProvider)) { + throw UserException.UserAlreadyRegisteredException() + } + val registerUser = User( + nickname = userClaims.username, + permission = UserPermission( + command.servicePermission, + command.privatePermission, + command.marketingPermission + ) + ) + val userAuth = UserAuth( + userId = registerUser.id, + socialId = userClaims.socialId, + socialLoginProvider = userClaims.socialLoginProvider + ) + + userManagementPort.saveUser(registerUser) + userAuthManagementPort.saveUserAuth(userAuth) + + return RegisterUserUsecase.Response( + userTokenManagementPort.generateAccessToken(registerUser), + userTokenManagementPort.generateRefreshToken(registerUser) + ) + } +} \ No newline at end of file diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/service/SocialLoginService.kt b/Application-Module/src/main/kotlin/com/asap/application/user/service/SocialLoginService.kt new file mode 100644 index 0000000..7e03800 --- /dev/null +++ b/Application-Module/src/main/kotlin/com/asap/application/user/service/SocialLoginService.kt @@ -0,0 +1,44 @@ +package com.asap.application.user.service + +import com.asap.application.user.port.`in`.SocialLoginUsecase +import com.asap.application.user.port.out.AuthInfoRetrievePort +import com.asap.application.user.port.out.UserAuthManagementPort +import com.asap.application.user.port.out.UserManagementPort +import com.asap.application.user.port.out.UserTokenManagementPort +import com.asap.common.exception.DefaultException +import com.asap.domain.user.enums.SocialLoginProvider +import org.springframework.stereotype.Service + + +@Service +class SocialLoginService( + private val userAuthManagementPort: UserAuthManagementPort, + private val authInfoRetrievePort: AuthInfoRetrievePort, + private val userTokenManagementPort: UserTokenManagementPort, + private val userManagementPort: UserManagementPort +) : SocialLoginUsecase { + + override fun login(command: SocialLoginUsecase.Command): SocialLoginUsecase.Response { + val authInfo = + authInfoRetrievePort.getAuthInfo(SocialLoginProvider.parse(command.provider), command.accessToken) + val userAuth = userAuthManagementPort.getUserAuth(authInfo.socialId, authInfo.socialLoginProvider) + return userAuth?.let { + userManagementPort.getUser(userAuth.userId)?.let { + SocialLoginUsecase.Success( + userTokenManagementPort.generateAccessToken(it), + userTokenManagementPort.generateRefreshToken(it) + ) + } ?: run { + throw DefaultException.InvalidStateException("사용자 인증정보만 존재합니다. - ${userAuth.userId}") + } + } ?: run { + val registerToken = userTokenManagementPort.generateRegisterToken( + authInfo.socialId, + authInfo.socialLoginProvider.name, + authInfo.username + ) + SocialLoginUsecase.NonRegistered(registerToken) + } + } + +} \ No newline at end of file diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/vo/AuthInfo.kt b/Application-Module/src/main/kotlin/com/asap/application/user/vo/AuthInfo.kt new file mode 100644 index 0000000..3beb090 --- /dev/null +++ b/Application-Module/src/main/kotlin/com/asap/application/user/vo/AuthInfo.kt @@ -0,0 +1,10 @@ +package com.asap.application.user.vo + +import com.asap.domain.user.enums.SocialLoginProvider + +data class AuthInfo( + val socialLoginProvider: SocialLoginProvider, + val socialId: String, + val username: String +) { +} \ No newline at end of file diff --git a/Application-Module/src/main/kotlin/com/asap/application/user/vo/UserClaims.kt b/Application-Module/src/main/kotlin/com/asap/application/user/vo/UserClaims.kt new file mode 100644 index 0000000..c9c348f --- /dev/null +++ b/Application-Module/src/main/kotlin/com/asap/application/user/vo/UserClaims.kt @@ -0,0 +1,13 @@ +package com.asap.application.user.vo + +import com.asap.domain.user.enums.SocialLoginProvider + +class UserClaims { + + data class Register( + val socialId: String, + val socialLoginProvider: SocialLoginProvider, + val username: String + ) + +} \ No newline at end of file diff --git a/Application-Module/src/test/kotlin/com/asap/application/user/service/RegisterUserServiceTest.kt b/Application-Module/src/test/kotlin/com/asap/application/user/service/RegisterUserServiceTest.kt new file mode 100644 index 0000000..e6a6758 --- /dev/null +++ b/Application-Module/src/test/kotlin/com/asap/application/user/service/RegisterUserServiceTest.kt @@ -0,0 +1,103 @@ +package com.asap.application.user.service + +import com.asap.application.user.exception.UserException +import com.asap.application.user.port.`in`.RegisterUserUsecase +import com.asap.application.user.port.out.UserAuthManagementPort +import com.asap.application.user.port.out.UserManagementPort +import com.asap.application.user.port.out.UserTokenManagementPort +import com.asap.application.user.vo.UserClaims +import com.asap.common.exception.DefaultException +import com.asap.domain.user.enums.SocialLoginProvider +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import java.time.LocalDate + + +class RegisterUserServiceTest: BehaviorSpec({ + + val mockUserManagementPort = mockk(relaxed = true) + val mockUserAuthManagementPort = mockk(relaxed=true) + val mockUserTokenManagementPort = mockk() + + + val registerUserService = RegisterUserService(mockUserTokenManagementPort, mockUserAuthManagementPort, mockUserManagementPort) + + + given("회원 가입 요청이 들어왔을 때") { + val successCommand = RegisterUserUsecase.Command("valid", true, true, true, LocalDate.now()) + every { mockUserTokenManagementPort.resolveRegisterToken("valid") } returns UserClaims.Register( + socialId = "123", + socialLoginProvider = SocialLoginProvider.KAKAO, + username = "test" + ) + every { mockUserAuthManagementPort.isExistsUserAuth("123", SocialLoginProvider.KAKAO) } returns false + every { mockUserTokenManagementPort.generateAccessToken(any()) } returns "accessToken" + every { mockUserTokenManagementPort.generateRefreshToken(any()) } returns "refreshToken" + `when`("회원 가입이 성공하면") { + val response = registerUserService.registerUser(successCommand) + then("access token과 refresh token을 반환한다.") { + response.accessToken.isNotEmpty() shouldBe true + response.refreshToken.isNotEmpty() shouldBe true + verify { mockUserManagementPort.saveUser(any()) } + } + } + + + every { mockUserTokenManagementPort.resolveRegisterToken("duplicate") } returns UserClaims.Register( + socialId = "duplicate", + socialLoginProvider = SocialLoginProvider.KAKAO, + username = "test" + ) + every { mockUserAuthManagementPort.isExistsUserAuth("duplicate", SocialLoginProvider.KAKAO) } returns true + `when`("중복 가입 요청이 들어왔을 때") { + val failCommand = RegisterUserUsecase.Command("duplicate", true, true, true, LocalDate.now()) + then("UserAlreadyRegisteredException 예외가 발생한다.") { + shouldThrow { + registerUserService.registerUser(failCommand) + } + } + } + + every { mockUserTokenManagementPort.resolveRegisterToken("invalid") } throws IllegalArgumentException("Invalid token") + `when`("register token이 유요하지 않다면") { + val failCommandWithoutRegisterToken = + RegisterUserUsecase.Command("invalid", true, true, true, LocalDate.now()) + then("예외가 발생한다.") { + shouldThrow { + registerUserService.registerUser(failCommandWithoutRegisterToken) + } + } + } + + + every { mockUserTokenManagementPort.resolveRegisterToken("valid") } returns UserClaims.Register( + socialId = "123", + socialLoginProvider = SocialLoginProvider.KAKAO, + username = "test" + ) + every{ mockUserAuthManagementPort.isExistsUserAuth("123", SocialLoginProvider.KAKAO) } returns false + `when`("서비스 동의를 하지 않았다면") { + val failCommandWithoutServicePermission = RegisterUserUsecase.Command("valid", false, true, true, LocalDate.now()) + then("InvalidPropertyException 예외가 발생한다.") { + shouldThrow { + registerUserService.registerUser(failCommandWithoutServicePermission) + } + } + } + + `when`("개인정보 동의를 하지 않았다면") { + val failCommandWithoutPrivatePermission = RegisterUserUsecase.Command("valid", true, false, true, LocalDate.now()) + then("InvalidPropertyException 예외가 발생한다.") { + shouldThrow { + registerUserService.registerUser(failCommandWithoutPrivatePermission) + } + } + } + } + +}) { +} \ No newline at end of file diff --git a/Application-Module/src/test/kotlin/com/asap/application/user/service/SocialLoginServiceTest.kt b/Application-Module/src/test/kotlin/com/asap/application/user/service/SocialLoginServiceTest.kt new file mode 100644 index 0000000..4b6ea76 --- /dev/null +++ b/Application-Module/src/test/kotlin/com/asap/application/user/service/SocialLoginServiceTest.kt @@ -0,0 +1,90 @@ +package com.asap.application.user.service + +import com.asap.application.user.port.`in`.SocialLoginUsecase +import com.asap.application.user.port.out.AuthInfoRetrievePort +import com.asap.application.user.port.out.UserAuthManagementPort +import com.asap.application.user.port.out.UserManagementPort +import com.asap.application.user.port.out.UserTokenManagementPort +import com.asap.application.user.vo.AuthInfo +import com.asap.common.exception.DefaultException +import com.asap.domain.common.DomainId +import com.asap.domain.user.entity.User +import com.asap.domain.user.entity.UserAuth +import com.asap.domain.user.enums.SocialLoginProvider +import com.asap.domain.user.vo.UserPermission +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf +import io.mockk.every +import io.mockk.mockk + +class SocialLoginServiceTest : BehaviorSpec({ + + val mockUserAuthManagementPort = mockk() + val mockAuthInfoRetrievePort = mockk() + val mockUserManagementPort = mockk() + val mockUserTokenManagementPort = mockk() + + val socialLoginService = SocialLoginService( + mockUserAuthManagementPort, + mockAuthInfoRetrievePort, + mockUserTokenManagementPort, + mockUserManagementPort + ) + + given("소셜 로그인에서 요청한 사용자가") { + var command = SocialLoginUsecase.Command(SocialLoginProvider.KAKAO.name, "registered") + val authInfo = AuthInfo(SocialLoginProvider.KAKAO, "socialId", "name") + val getUserAuth = UserAuth( + userId = DomainId.generate(), + socialId = "socialId", + socialLoginProvider = SocialLoginProvider.KAKAO + ) + val getUser = User( + id = getUserAuth.userId, + nickname = authInfo.username, + permission = UserPermission(true, true, true) + ) + every { mockAuthInfoRetrievePort.getAuthInfo(SocialLoginProvider.KAKAO, "registered") } returns authInfo + every { + mockUserAuthManagementPort.getUserAuth( + authInfo.socialId, + authInfo.socialLoginProvider + ) + } returns getUserAuth + every{ mockUserManagementPort.getUser(any()) } returns getUser + every { mockUserTokenManagementPort.generateAccessToken(getUser) } returns "accessToken" + every { mockUserTokenManagementPort.generateRefreshToken(getUser) } returns "refreshToken" + `when`("기존에 존재한다면") { + val response = socialLoginService.login(command) + then("access token과 refresh token을 반환하는 success 인스턴스를 반환한다.") { + response.shouldBeInstanceOf() + response.accessToken.isNotEmpty() shouldBe true + response.refreshToken.isNotEmpty() shouldBe true + } + } + + every { mockUserManagementPort.getUser(any()) } returns null + `when`("인증 정보만 존재하면 존재하고 사용자 정보가 없다면") { + + then("InvalidStateException 예외가 발생한다.") { + shouldThrow { + socialLoginService.login(command) + } + } + } + + command = SocialLoginUsecase.Command(SocialLoginProvider.KAKAO.name, "nonRegistered") + every { mockAuthInfoRetrievePort.getAuthInfo(SocialLoginProvider.KAKAO, "nonRegistered") } returns authInfo + every { mockUserAuthManagementPort.getUserAuth(authInfo.socialId, authInfo.socialLoginProvider) } returns null + every { mockUserTokenManagementPort.generateRegisterToken(authInfo.socialId, authInfo.socialLoginProvider.name, authInfo.username) } returns "registerToken" + `when`("가입되지 않았다면") { + val response = socialLoginService.login(command) + then("register token을 반환하는 nonRegistered 인스턴스를 반환한다.") { + response.shouldBeInstanceOf() + response.registerToken.isNotEmpty() shouldBe true + } + } + } +}) \ No newline at end of file From fbbd0a78588d5ac353197880f6a14d1fbeed506c Mon Sep 17 00:00:00 2001 From: Sim-km Date: Thu, 29 Aug 2024 00:20:53 +0900 Subject: [PATCH 6/8] =?UTF-8?q?ASAP-60=20feat:=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EC=98=88=EC=99=B8=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/BusinessException.kt | 20 +++++++++++++ .../asap/common/exception/DefaultException.kt | 30 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 Common-Module/src/main/kotlin/com/asap/common/exception/BusinessException.kt create mode 100644 Common-Module/src/main/kotlin/com/asap/common/exception/DefaultException.kt diff --git a/Common-Module/src/main/kotlin/com/asap/common/exception/BusinessException.kt b/Common-Module/src/main/kotlin/com/asap/common/exception/BusinessException.kt new file mode 100644 index 0000000..8871023 --- /dev/null +++ b/Common-Module/src/main/kotlin/com/asap/common/exception/BusinessException.kt @@ -0,0 +1,20 @@ +package com.asap.common.exception + +abstract class BusinessException( + codePrefix: String = CODE_PREFIX, + errorCode: Int, + httpStatus: Int = DEFAULT_HTTP_STATUS, + override val message: String = DEFAULT_ERROR_MESSAGE +) : RuntimeException(message){ + + val code: String = "$codePrefix-${errorCode.toString().padStart(DEFAULT_CODE_LENGTH, DEFAULT_CODE_PAD)}" + val httpStatus: Int = httpStatus + + companion object{ + const val CODE_PREFIX = "UNEXPECTED" + const val DEFAULT_ERROR_MESSAGE = "예상하지 못한 예외가 발생했습니다." + const val DEFAULT_HTTP_STATUS = 500 + const val DEFAULT_CODE_LENGTH = 3 + const val DEFAULT_CODE_PAD = '0' + } +} \ No newline at end of file diff --git a/Common-Module/src/main/kotlin/com/asap/common/exception/DefaultException.kt b/Common-Module/src/main/kotlin/com/asap/common/exception/DefaultException.kt new file mode 100644 index 0000000..6ea397e --- /dev/null +++ b/Common-Module/src/main/kotlin/com/asap/common/exception/DefaultException.kt @@ -0,0 +1,30 @@ +package com.asap.common.exception + +sealed class DefaultException( + codePrefix: String = CODE_PREFIX, + errorCode: Int, + httpStatus: Int = 400, + message: String = DEFAULT_ERROR_MESSAGE +): BusinessException(codePrefix, errorCode, httpStatus, message) { + + class InvalidDefaultException(message: String = "유효하지 않은 프로퍼티입니다." ): DefaultException( + errorCode = 1, + message = message + ) + + class InvalidArgumentException(message: String = "유효하지 않은 값입니다." ): DefaultException( + errorCode = 2, + message = message + ) + + class InvalidStateException(message: String = "유효하지 않은 상태입니다." ): DefaultException( + errorCode = 3, + httpStatus = 500, + message = message + ) + + companion object{ + const val CODE_PREFIX = "DEFAULT" + const val DEFAULT_ERROR_MESSAGE = "프로퍼티와 관련된 예외가 발생했습니다." + } +} \ No newline at end of file From d001d6f517d44c326e50ee66fa406e931a21223e Mon Sep 17 00:00:00 2001 From: Sim-km Date: Thu, 29 Aug 2024 00:24:01 +0900 Subject: [PATCH 7/8] =?UTF-8?q?ASAP-60=20chore:=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Bootstrap 모듈 의존성 정의(usecase, common) - 모듈 설정 추가(settings.gradles.kts) --- Bootstrap-Module/README.md | 13 ++++++++----- Bootstrap-Module/build.gradle.kts | 4 ++++ .../com/asap/bootstrap/BootstrapApplication.kt | 18 ++++++++++++++++++ build.gradle.kts | 8 +++++++- settings.gradle.kts | 5 ++++- 5 files changed, 41 insertions(+), 7 deletions(-) create mode 100644 Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/BootstrapApplication.kt diff --git a/Bootstrap-Module/README.md b/Bootstrap-Module/README.md index a166f62..a9de4fa 100644 --- a/Bootstrap-Module/README.md +++ b/Bootstrap-Module/README.md @@ -9,13 +9,16 @@ ```markdown . -└── {domain}/ - ├── api - ├── controller - └── dto +├── {domain}/ +│ ├── api +│ ├── controller +│ └── dto +└── common ``` -* `{domain}`: 도메인 이름을 의미합니다. 예를 들어, `auth`, `user` 등이 될 수 있습니다. +* `{domain}`: 도메인 이름을 의미합니다. 예를 들어, `auth`, `user` 등이 될 수 있습니다. * `api`: API 스팩을 정의합니다. * `controller`: api 스팩에 대한 구현체입니다. * `dto`: 요청간 전달되는 데이터를 정의합니다. +* `common`: 공통으로 사용되는 클래스를 정의합니다. + diff --git a/Bootstrap-Module/build.gradle.kts b/Bootstrap-Module/build.gradle.kts index 1e30d5c..65da8c9 100644 --- a/Bootstrap-Module/build.gradle.kts +++ b/Bootstrap-Module/build.gradle.kts @@ -2,4 +2,8 @@ dependencies{ implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0") + + + implementation(project(":Application-Module")) + implementation(project(":Common-Module")) } \ No newline at end of file diff --git a/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/BootstrapApplication.kt b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/BootstrapApplication.kt new file mode 100644 index 0000000..d31feca --- /dev/null +++ b/Bootstrap-Module/src/main/kotlin/com/asap/bootstrap/BootstrapApplication.kt @@ -0,0 +1,18 @@ +package com.asap.bootstrap + +import com.asap.application.ApplicationConfig +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.context.annotation.Import + +@SpringBootApplication +@Import( + value = [ + ApplicationConfig::class, + ] +) +class BootstrapApplication {} + +fun main(args: Array) { + SpringApplication.run(BootstrapApplication::class.java, *args) +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 67946d0..9bf505d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,7 +17,8 @@ allprojects { version = "" val javaVersion = "17" - val kotestVersion = "5.0.0" + val kotestVersion = "5.9.1" + val mockkVersion = "1.13.12" tasks.withType { sourceCompatibility = javaVersion @@ -60,6 +61,11 @@ allprojects { // kotest testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") + testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") + testImplementation("io.kotest:kotest-property:$kotestVersion") + + // mockk + testImplementation("io.mockk:mockk:${mockkVersion}") // jackson implementation("com.fasterxml.jackson.module:jackson-module-kotlin") diff --git a/settings.gradle.kts b/settings.gradle.kts index d8a26be..23a6619 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,6 @@ rootProject.name = "Lettering-Backend" -include(":Bootstrap-Module") \ No newline at end of file +include(":Bootstrap-Module") +include(":Application-Module") +include(":Domain-Module") +include(":Common-Module") \ No newline at end of file From 04d234053409a715095688063ed92c04aec96473 Mon Sep 17 00:00:00 2001 From: Sim-km Date: Thu, 29 Aug 2024 00:45:12 +0900 Subject: [PATCH 8/8] =?UTF-8?q?ASAP-60=20chore:=20Dockerfile=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .deploy/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.deploy/Dockerfile b/.deploy/Dockerfile index 0d5c38a..a2c1510 100644 --- a/.deploy/Dockerfile +++ b/.deploy/Dockerfile @@ -1,6 +1,6 @@ FROM amazoncorretto:17-alpine-jdk -ARG TARGET_JAR=/app/build/libs/Bootstrap-Module.jar +ARG TARGET_JAR=/Bootstrap-Module/build/libs/Bootstrap-Module.jar COPY ${TARGET_JAR} /application.jar