From e1ced3faee6294568903f7a5180dded0a45b3f30 Mon Sep 17 00:00:00 2001 From: habin Date: Wed, 29 May 2024 20:59:00 +0900 Subject: [PATCH 1/6] feat(account) email MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor: 기존 Email API Endpoint EmailController로 병합 --- .../rest/CheckEmailVerifiedController.java | 33 ---------- ...teController.java => EmailController.java} | 14 ++++- .../rest/CheckEmailVerifiedControllerIT.java | 63 ------------------- ...ntrollerIT.java => EmailControllerIT.java} | 41 +++++++++++- 4 files changed, 53 insertions(+), 98 deletions(-) delete mode 100644 src/main/java/com/tune_fun/v1/account/adapter/input/rest/CheckEmailVerifiedController.java rename src/main/java/com/tune_fun/v1/account/adapter/input/rest/{CheckEmailDuplicateController.java => EmailController.java} (68%) delete mode 100644 src/test/java/com/tune_fun/v1/account/adapter/input/rest/CheckEmailVerifiedControllerIT.java rename src/test/java/com/tune_fun/v1/account/adapter/input/rest/{CheckEmailDuplicateControllerIT.java => EmailControllerIT.java} (67%) diff --git a/src/main/java/com/tune_fun/v1/account/adapter/input/rest/CheckEmailVerifiedController.java b/src/main/java/com/tune_fun/v1/account/adapter/input/rest/CheckEmailVerifiedController.java deleted file mode 100644 index 34000e17..00000000 --- a/src/main/java/com/tune_fun/v1/account/adapter/input/rest/CheckEmailVerifiedController.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.tune_fun.v1.account.adapter.input.rest; - - -import com.tune_fun.v1.account.application.port.input.usecase.CheckEmailVerifiedUseCase; -import com.tune_fun.v1.account.domain.value.CurrentUser; -import com.tune_fun.v1.common.config.Uris; -import com.tune_fun.v1.common.hexagon.WebAdapter; -import com.tune_fun.v1.common.response.BasePayload; -import com.tune_fun.v1.common.response.Response; -import com.tune_fun.v1.common.response.ResponseMapper; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.userdetails.User; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -import static com.tune_fun.v1.common.response.MessageCode.SUCCESS_EMAIL_VERIFIED; - -@RestController -@WebAdapter -@RequiredArgsConstructor -public class CheckEmailVerifiedController { - - private final CheckEmailVerifiedUseCase checkEmailVerifiedUseCase; - private final ResponseMapper responseMapper; - - @GetMapping(value = Uris.CHECK_EMAIL_VERIFIED) - public ResponseEntity> checkEmailVerified(@CurrentUser User user) { - checkEmailVerifiedUseCase.checkEmailVerified(user); - return responseMapper.ok(SUCCESS_EMAIL_VERIFIED); - } - -} diff --git a/src/main/java/com/tune_fun/v1/account/adapter/input/rest/CheckEmailDuplicateController.java b/src/main/java/com/tune_fun/v1/account/adapter/input/rest/EmailController.java similarity index 68% rename from src/main/java/com/tune_fun/v1/account/adapter/input/rest/CheckEmailDuplicateController.java rename to src/main/java/com/tune_fun/v1/account/adapter/input/rest/EmailController.java index b24e41c6..b9db6b92 100644 --- a/src/main/java/com/tune_fun/v1/account/adapter/input/rest/CheckEmailDuplicateController.java +++ b/src/main/java/com/tune_fun/v1/account/adapter/input/rest/EmailController.java @@ -1,6 +1,8 @@ package com.tune_fun.v1.account.adapter.input.rest; import com.tune_fun.v1.account.application.port.input.usecase.CheckEmailDuplicateUseCase; +import com.tune_fun.v1.account.application.port.input.usecase.CheckEmailVerifiedUseCase; +import com.tune_fun.v1.account.domain.value.CurrentUser; import com.tune_fun.v1.common.config.Uris; import com.tune_fun.v1.common.hexagon.WebAdapter; import com.tune_fun.v1.common.response.BasePayload; @@ -9,20 +11,24 @@ import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.userdetails.User; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import static com.tune_fun.v1.common.response.MessageCode.SUCCESS_EMAIL_UNIQUE; +import static com.tune_fun.v1.common.response.MessageCode.SUCCESS_EMAIL_VERIFIED; @RestController @WebAdapter @Validated @RequiredArgsConstructor -public class CheckEmailDuplicateController { +public class EmailController { private final CheckEmailDuplicateUseCase checkEmailDuplicateUseCase; + private final CheckEmailVerifiedUseCase checkEmailVerifiedUseCase; + private final ResponseMapper responseMapper; @GetMapping(value = Uris.CHECK_EMAIL_DUPLICATE) @@ -32,4 +38,10 @@ public ResponseEntity> checkUsernameDuplicate(@RequestPara return responseMapper.ok(SUCCESS_EMAIL_UNIQUE); } + @GetMapping(value = Uris.CHECK_EMAIL_VERIFIED) + public ResponseEntity> checkEmailVerified(@CurrentUser User user) { + checkEmailVerifiedUseCase.checkEmailVerified(user); + return responseMapper.ok(SUCCESS_EMAIL_VERIFIED); + } + } diff --git a/src/test/java/com/tune_fun/v1/account/adapter/input/rest/CheckEmailVerifiedControllerIT.java b/src/test/java/com/tune_fun/v1/account/adapter/input/rest/CheckEmailVerifiedControllerIT.java deleted file mode 100644 index 523d6a7d..00000000 --- a/src/test/java/com/tune_fun/v1/account/adapter/input/rest/CheckEmailVerifiedControllerIT.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.tune_fun.v1.account.adapter.input.rest; - -import com.tune_fun.v1.base.ControllerBaseTest; -import com.tune_fun.v1.common.config.Uris; -import com.tune_fun.v1.common.response.MessageCode; -import com.tune_fun.v1.dummy.DummyService; -import com.tune_fun.v1.otp.adapter.output.persistence.OtpType; -import com.tune_fun.v1.otp.domain.value.CurrentDecryptedOtp; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; - -import static com.epages.restdocs.apispec.ResourceDocumentation.resource; -import static com.epages.restdocs.apispec.ResourceSnippetParameters.builder; -import static org.springframework.http.HttpHeaders.AUTHORIZATION; -import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; - -class CheckEmailVerifiedControllerIT extends ControllerBaseTest { - - @Autowired - private DummyService dummyService; - - @Transactional - @Test - @Order(1) - @DisplayName("이메일 인증 여부 확인, 성공") - void checkEmailVerifiedSuccess() throws Exception { - dummyService.initAccount(); - dummyService.forgotPasswordOtp(); - - CurrentDecryptedOtp forgotPasswordOtp = dummyService.getForgotPasswordOtp(); - dummyService.verifyOtp(OtpType.FORGOT_PASSWORD, forgotPasswordOtp.token()); - - dummyService.login(dummyService.getDefaultAccount()); - String accessToken = dummyService.getDefaultAccessToken(); - - mockMvc.perform( - get(Uris.CHECK_EMAIL_VERIFIED) - .header(AUTHORIZATION, bearerToken(accessToken)) - ) - .andExpectAll(baseAssertion(MessageCode.SUCCESS_EMAIL_VERIFIED)) - .andDo( - restDocs.document( - requestHeaders(authorizationHeader), - responseFields(baseResponseFields), - resource( - builder(). - description("이메일 인증 여부 확인"). - requestHeaders(authorizationHeader). - responseFields(baseResponseFields) - .build() - ) - ) - ); - - } - - -} diff --git a/src/test/java/com/tune_fun/v1/account/adapter/input/rest/CheckEmailDuplicateControllerIT.java b/src/test/java/com/tune_fun/v1/account/adapter/input/rest/EmailControllerIT.java similarity index 67% rename from src/test/java/com/tune_fun/v1/account/adapter/input/rest/CheckEmailDuplicateControllerIT.java rename to src/test/java/com/tune_fun/v1/account/adapter/input/rest/EmailControllerIT.java index bd8f3e33..2e3d892c 100644 --- a/src/test/java/com/tune_fun/v1/account/adapter/input/rest/CheckEmailDuplicateControllerIT.java +++ b/src/test/java/com/tune_fun/v1/account/adapter/input/rest/EmailControllerIT.java @@ -5,6 +5,8 @@ import com.tune_fun.v1.common.config.Uris; import com.tune_fun.v1.common.response.MessageCode; import com.tune_fun.v1.dummy.DummyService; +import com.tune_fun.v1.otp.adapter.output.persistence.OtpType; +import com.tune_fun.v1.otp.domain.value.CurrentDecryptedOtp; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -16,12 +18,14 @@ import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static com.epages.restdocs.apispec.ResourceSnippetParameters.builder; import static com.tune_fun.v1.base.doc.RestDocsConfig.constraint; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; -class CheckEmailDuplicateControllerIT extends ControllerBaseTest { +class EmailControllerIT extends ControllerBaseTest { @Autowired private DummyService dummyService; @@ -86,4 +90,39 @@ void checkEmailDuplicateFailed() throws Exception { ); } + @Transactional + @Test + @Order(3) + @DisplayName("이메일 인증 여부 확인, 성공") + void checkEmailVerifiedSuccess() throws Exception { + dummyService.initAccount(); + dummyService.forgotPasswordOtp(); + + CurrentDecryptedOtp forgotPasswordOtp = dummyService.getForgotPasswordOtp(); + dummyService.verifyOtp(OtpType.FORGOT_PASSWORD, forgotPasswordOtp.token()); + + dummyService.login(dummyService.getDefaultAccount()); + String accessToken = dummyService.getDefaultAccessToken(); + + mockMvc.perform( + get(Uris.CHECK_EMAIL_VERIFIED) + .header(AUTHORIZATION, bearerToken(accessToken)) + ) + .andExpectAll(baseAssertion(MessageCode.SUCCESS_EMAIL_VERIFIED)) + .andDo( + restDocs.document( + requestHeaders(authorizationHeader), + responseFields(baseResponseFields), + resource( + builder(). + description("이메일 인증 여부 확인"). + requestHeaders(authorizationHeader). + responseFields(baseResponseFields) + .build() + ) + ) + ); + + } + } From fec3a0c402248254c51ac6cff23b2b45fc2e691b Mon Sep 17 00:00:00 2001 From: habin Date: Thu, 30 May 2024 00:08:58 +0900 Subject: [PATCH 2/6] feat(account) email MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chore: RegisterEmailOtp Endpoint 정의 --- .../adapter/input/rest/EmailController.java | 13 ++++- .../port/input/command/AccountCommands.java | 4 ++ .../input/usecase/RegisterEmailUseCase.java | 11 ++++ .../port/output/SaveEmailPort.java | 4 ++ .../service/RegisterEmailService.java | 58 +++++++++++++++++++ .../com/tune_fun/v1/common/config/Uris.java | 1 + 6 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/tune_fun/v1/account/application/port/input/usecase/RegisterEmailUseCase.java create mode 100644 src/main/java/com/tune_fun/v1/account/application/port/output/SaveEmailPort.java create mode 100644 src/main/java/com/tune_fun/v1/account/application/service/RegisterEmailService.java diff --git a/src/main/java/com/tune_fun/v1/account/adapter/input/rest/EmailController.java b/src/main/java/com/tune_fun/v1/account/adapter/input/rest/EmailController.java index b9db6b92..927e379c 100644 --- a/src/main/java/com/tune_fun/v1/account/adapter/input/rest/EmailController.java +++ b/src/main/java/com/tune_fun/v1/account/adapter/input/rest/EmailController.java @@ -1,21 +1,22 @@ package com.tune_fun.v1.account.adapter.input.rest; +import com.tune_fun.v1.account.application.port.input.command.AccountCommands; import com.tune_fun.v1.account.application.port.input.usecase.CheckEmailDuplicateUseCase; import com.tune_fun.v1.account.application.port.input.usecase.CheckEmailVerifiedUseCase; +import com.tune_fun.v1.account.application.port.input.usecase.RegisterEmailUseCase; import com.tune_fun.v1.account.domain.value.CurrentUser; import com.tune_fun.v1.common.config.Uris; import com.tune_fun.v1.common.hexagon.WebAdapter; import com.tune_fun.v1.common.response.BasePayload; import com.tune_fun.v1.common.response.Response; import com.tune_fun.v1.common.response.ResponseMapper; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.userdetails.User; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import static com.tune_fun.v1.common.response.MessageCode.SUCCESS_EMAIL_UNIQUE; import static com.tune_fun.v1.common.response.MessageCode.SUCCESS_EMAIL_VERIFIED; @@ -28,6 +29,7 @@ public class EmailController { private final CheckEmailDuplicateUseCase checkEmailDuplicateUseCase; private final CheckEmailVerifiedUseCase checkEmailVerifiedUseCase; + private final RegisterEmailUseCase registerEmailUseCase; private final ResponseMapper responseMapper; @@ -44,4 +46,9 @@ public ResponseEntity> checkEmailVerified(@CurrentUser Use return responseMapper.ok(SUCCESS_EMAIL_VERIFIED); } + @PostMapping(value = Uris.EMAIL_ROOT) + public ResponseEntity sendRegisterEmailOtp(@Valid @RequestBody AccountCommands.SaveEmail command, @CurrentUser User user) { + return null; + } + } diff --git a/src/main/java/com/tune_fun/v1/account/application/port/input/command/AccountCommands.java b/src/main/java/com/tune_fun/v1/account/application/port/input/command/AccountCommands.java index 95770ef5..72c9d2d9 100644 --- a/src/main/java/com/tune_fun/v1/account/application/port/input/command/AccountCommands.java +++ b/src/main/java/com/tune_fun/v1/account/application/port/input/command/AccountCommands.java @@ -72,4 +72,8 @@ public record SetNewPassword(@NotBlank(message = "{new_password.not_blank}") Str public record UpdateNickname(@NotBlank(message = "{new_nickname.not_blank}") String newNickname) { } + @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) + public record SaveEmail(@NotBlank(message = "{email.not_blank}") String email) { + } + } diff --git a/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/RegisterEmailUseCase.java b/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/RegisterEmailUseCase.java new file mode 100644 index 00000000..c6f44cf0 --- /dev/null +++ b/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/RegisterEmailUseCase.java @@ -0,0 +1,11 @@ +package com.tune_fun.v1.account.application.port.input.usecase; + +import com.tune_fun.v1.account.application.port.input.command.AccountCommands; +import org.springframework.security.core.userdetails.User; + +@FunctionalInterface +public interface RegisterEmailUseCase { + + void registerEmail(final AccountCommands.Register command, final User user) throws Exception; + +} diff --git a/src/main/java/com/tune_fun/v1/account/application/port/output/SaveEmailPort.java b/src/main/java/com/tune_fun/v1/account/application/port/output/SaveEmailPort.java new file mode 100644 index 00000000..a43861bc --- /dev/null +++ b/src/main/java/com/tune_fun/v1/account/application/port/output/SaveEmailPort.java @@ -0,0 +1,4 @@ +package com.tune_fun.v1.account.application.port.output; + +public interface SaveEmailPort { +} diff --git a/src/main/java/com/tune_fun/v1/account/application/service/RegisterEmailService.java b/src/main/java/com/tune_fun/v1/account/application/service/RegisterEmailService.java new file mode 100644 index 00000000..f3a55463 --- /dev/null +++ b/src/main/java/com/tune_fun/v1/account/application/service/RegisterEmailService.java @@ -0,0 +1,58 @@ +package com.tune_fun.v1.account.application.service; + +import com.tune_fun.v1.account.application.port.input.command.AccountCommands; +import com.tune_fun.v1.account.application.port.input.usecase.RegisterEmailUseCase; +import com.tune_fun.v1.account.application.port.output.LoadAccountPort; +import com.tune_fun.v1.account.application.port.output.SaveEmailPort; +import com.tune_fun.v1.account.domain.value.CurrentAccount; +import com.tune_fun.v1.common.exception.CommonApplicationException; +import com.tune_fun.v1.common.hexagon.UseCase; +import com.tune_fun.v1.otp.application.port.output.SaveOtpPort; +import com.tune_fun.v1.otp.application.port.output.SendOtpPort; +import com.tune_fun.v1.otp.domain.behavior.SaveOtp; +import com.tune_fun.v1.otp.domain.behavior.SendOtp; +import com.tune_fun.v1.otp.domain.value.CurrentOtp; +import lombok.RequiredArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.tune_fun.v1.common.response.MessageCode.ACCOUNT_NOT_FOUND; +import static com.tune_fun.v1.otp.adapter.output.persistence.OtpType.FORGOT_PASSWORD; +import static com.tune_fun.v1.otp.adapter.output.persistence.OtpType.VERIFY_EMAIL; + +@Service +@UseCase +@RequiredArgsConstructor +public class RegisterEmailService implements RegisterEmailUseCase { + + private final LoadAccountPort loadAccountPort; + private final SaveEmailPort saveEmailPort; + + private final SaveOtpPort saveOtpPort; + private final SendOtpPort sendOtpPort; + + @Transactional + @Override + public void registerEmail(AccountCommands.Register command, User user) throws Exception { + CurrentAccount currentAccount = getCurrentAccount(command); + + SaveOtp saveOtp = getSaveOtp(user.getUsername()); + CurrentOtp currentOtp = saveOtpPort.saveOtp(saveOtp); + + SendOtp sendOtp = new SendOtp(currentAccount.email(), currentAccount.nickname(), currentOtp.token()); + sendOtpPort.sendOtp(sendOtp); + } + + @NotNull + private static SaveOtp getSaveOtp(String username) { + return new SaveOtp(username, VERIFY_EMAIL.getLabel()); + } + + @Transactional(readOnly = true) + public CurrentAccount getCurrentAccount(final AccountCommands.Register command) { + return loadAccountPort.currentAccountInfo(command.username()) + .orElseThrow(() -> new CommonApplicationException(ACCOUNT_NOT_FOUND)); + } +} diff --git a/src/main/java/com/tune_fun/v1/common/config/Uris.java b/src/main/java/com/tune_fun/v1/common/config/Uris.java index 77e10fd0..65ebeb1b 100644 --- a/src/main/java/com/tune_fun/v1/common/config/Uris.java +++ b/src/main/java/com/tune_fun/v1/common/config/Uris.java @@ -49,6 +49,7 @@ private Uris() { public static final String FORGOT_PASSWORD_SEND_OTP = API_V1_ROOT + "/forgot-password/send-otp"; public static final String CHECK_EMAIL_VERIFIED = API_V1_ROOT + "/check-email-verified"; + public static final String EMAIL_ROOT = API_V1_ROOT + "/accounts/email"; public static final String VERIFY_OTP = API_V1_ROOT + "/otp/verify"; public static final String VERIFY_OTP_WITH_TOKEN = API_V1_ROOT + "/otp/verify-with-token"; From a7f3b915c598aabc99037ba61c69db88fd30b9ec Mon Sep 17 00:00:00 2001 From: habin Date: Sat, 1 Jun 2024 14:07:49 +0900 Subject: [PATCH 3/6] configuration(external) AWS CloudWatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix: AWS CloudWatch 로그그룹 및 로그스트림 명 수정 --- .../v1/common/util/i18n/MessageSourceUtil.java | 3 ++- src/main/resources/logback-spring.xml | 11 ++++++----- .../input/rest/UpdateNicknameControllerIT.java | 2 +- ...{LikeControllerTest.java => LikeControllerIT.java} | 10 +++++----- 4 files changed, 14 insertions(+), 12 deletions(-) rename src/test/java/com/tune_fun/v1/interaction/adapter/input/rest/{LikeControllerTest.java => LikeControllerIT.java} (95%) diff --git a/src/main/java/com/tune_fun/v1/common/util/i18n/MessageSourceUtil.java b/src/main/java/com/tune_fun/v1/common/util/i18n/MessageSourceUtil.java index 733d43fd..8174c881 100644 --- a/src/main/java/com/tune_fun/v1/common/util/i18n/MessageSourceUtil.java +++ b/src/main/java/com/tune_fun/v1/common/util/i18n/MessageSourceUtil.java @@ -3,6 +3,7 @@ import com.tune_fun.v1.common.response.MessageCode; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.stereotype.Component; import java.util.Locale; @@ -17,7 +18,7 @@ public MessageSourceUtil(@Qualifier("message") MessageSource messageSource) { } public String getMessage(String code) { - return messageSource.getMessage(code, null, Locale.getDefault()); + return messageSource.getMessage(code, null, LocaleContextHolder.getLocale()); } public String getMessage(MessageCode code) { diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 027c7b66..6f9bfc60 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -27,17 +27,18 @@ ${CUSTOM_LOG_PATTERN} - + DEV + - DEV_STANDALONE + DEV - + - DEV + DEV_STANDALONE - error- + TuneFun- 50 30000 5000 diff --git a/src/test/java/com/tune_fun/v1/account/adapter/input/rest/UpdateNicknameControllerIT.java b/src/test/java/com/tune_fun/v1/account/adapter/input/rest/UpdateNicknameControllerIT.java index 03b30062..35a734eb 100644 --- a/src/test/java/com/tune_fun/v1/account/adapter/input/rest/UpdateNicknameControllerIT.java +++ b/src/test/java/com/tune_fun/v1/account/adapter/input/rest/UpdateNicknameControllerIT.java @@ -36,7 +36,7 @@ class UpdateNicknameControllerIT extends ControllerBaseTest { @Transactional @Test @Order(1) - @DisplayName("닉네임, 성공") + @DisplayName("닉네임 변경, 성공") void updateNicknameSuccess() throws Exception { dummyService.initAndLogin(); diff --git a/src/test/java/com/tune_fun/v1/interaction/adapter/input/rest/LikeControllerTest.java b/src/test/java/com/tune_fun/v1/interaction/adapter/input/rest/LikeControllerIT.java similarity index 95% rename from src/test/java/com/tune_fun/v1/interaction/adapter/input/rest/LikeControllerTest.java rename to src/test/java/com/tune_fun/v1/interaction/adapter/input/rest/LikeControllerIT.java index 1c12bcf6..c7895025 100644 --- a/src/test/java/com/tune_fun/v1/interaction/adapter/input/rest/LikeControllerTest.java +++ b/src/test/java/com/tune_fun/v1/interaction/adapter/input/rest/LikeControllerIT.java @@ -23,7 +23,7 @@ import static java.util.concurrent.TimeUnit.SECONDS; import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.times; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.verify; import static org.springframework.http.HttpHeaders.AUTHORIZATION; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; @@ -36,7 +36,7 @@ @Slf4j @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) -class LikeControllerTest extends ControllerBaseTest { +class LikeControllerIT extends ControllerBaseTest { @Autowired private DummyService dummyService; @@ -139,7 +139,7 @@ private void awaitLikeCountAggregationScheduler() { } private void verifyInvokeAggregateLikeCount() { - verify(likeCountAggregationScheduler, times(1)).aggregateLikeCount(); + verify(likeCountAggregationScheduler, atLeastOnce()).aggregateLikeCount(); } private void awaitIncrementLikeCount(Long votePaperId) { @@ -147,7 +147,7 @@ private void awaitIncrementLikeCount(Long votePaperId) { } private void verifyInvokeIncrementLikeCount(Long votePaperId) { - verify(saveVotePaperStatPort, times(1)).updateLikeCount(votePaperId, 1L); + verify(saveVotePaperStatPort, atLeastOnce()).updateLikeCount(votePaperId, 1L); } private void awaitDecrementLikeCount(Long votePaperId) { @@ -155,6 +155,6 @@ private void awaitDecrementLikeCount(Long votePaperId) { } private void verifyInvokeDecrementLikeCount(Long votePaperId) { - verify(saveVotePaperStatPort, times(1)).updateLikeCount(votePaperId, 0L); + verify(saveVotePaperStatPort, atLeastOnce()).updateLikeCount(votePaperId, 0L); } } \ No newline at end of file From a46e4a39f199a65504fb5b12cff22fad975e3685 Mon Sep 17 00:00:00 2001 From: habin Date: Sat, 1 Jun 2024 14:19:02 +0900 Subject: [PATCH 4/6] feat(account) email MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit chore: 이메일 인증번호 발송, 이메일 변경, 이메일 해제 API 엔드포인트 추가 --- .../adapter/input/rest/EmailController.java | 18 +++++++++++++++++- .../com/tune_fun/v1/common/config/Uris.java | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/tune_fun/v1/account/adapter/input/rest/EmailController.java b/src/main/java/com/tune_fun/v1/account/adapter/input/rest/EmailController.java index 927e379c..e1215660 100644 --- a/src/main/java/com/tune_fun/v1/account/adapter/input/rest/EmailController.java +++ b/src/main/java/com/tune_fun/v1/account/adapter/input/rest/EmailController.java @@ -29,6 +29,7 @@ public class EmailController { private final CheckEmailDuplicateUseCase checkEmailDuplicateUseCase; private final CheckEmailVerifiedUseCase checkEmailVerifiedUseCase; + private final RegisterEmailUseCase registerEmailUseCase; private final ResponseMapper responseMapper; @@ -47,7 +48,22 @@ public ResponseEntity> checkEmailVerified(@CurrentUser Use } @PostMapping(value = Uris.EMAIL_ROOT) - public ResponseEntity sendRegisterEmailOtp(@Valid @RequestBody AccountCommands.SaveEmail command, @CurrentUser User user) { + public ResponseEntity registerEmail(@Valid @RequestBody AccountCommands.SaveEmail command, @CurrentUser User user) { + return null; + } + + @PostMapping(value = Uris.EMAIL_ROOT + "/verify") + public ResponseEntity verifyEmail(@CurrentUser User user) { + return null; + } + + @PatchMapping(value = Uris.EMAIL_ROOT) + public ResponseEntity changeEmail(@Valid @RequestBody AccountCommands.SaveEmail command, @CurrentUser User user) { + return null; + } + + @DeleteMapping(value = Uris.EMAIL_ROOT) + public ResponseEntity unlinkEmail(@CurrentUser User user) { return null; } diff --git a/src/main/java/com/tune_fun/v1/common/config/Uris.java b/src/main/java/com/tune_fun/v1/common/config/Uris.java index 65ebeb1b..ade7f41f 100644 --- a/src/main/java/com/tune_fun/v1/common/config/Uris.java +++ b/src/main/java/com/tune_fun/v1/common/config/Uris.java @@ -52,7 +52,7 @@ private Uris() { public static final String EMAIL_ROOT = API_V1_ROOT + "/accounts/email"; public static final String VERIFY_OTP = API_V1_ROOT + "/otp/verify"; - public static final String VERIFY_OTP_WITH_TOKEN = API_V1_ROOT + "/otp/verify-with-token"; + public static final String VERIFY_OTP_WITH_AUTHORIZATION = API_V1_ROOT + "/otp/verify-with-authorization"; public static final String RESEND_OTP = API_V1_ROOT + "/otp/resend"; public static final String VOTE_ROOT = API_V1_ROOT + "/votes"; From 2f068d2f69697d55f1741968ce1d2890d048ff47 Mon Sep 17 00:00:00 2001 From: habin Date: Sat, 1 Jun 2024 14:58:45 +0900 Subject: [PATCH 5/6] feat(account) email MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: 이메일 등록 API 구현 feat: 이메일 인증번호 발송 API 구현 feat: 이메일 변경 API 구현 feat: 이메일 해제 API 구현 --- .../adapter/input/rest/EmailController.java | 27 +++++++------ .../AccountPersistenceAdapter.java | 35 ++++++++++++++--- .../usecase/email/ChangeEmailUseCase.java | 9 +++++ .../CheckEmailDuplicateUseCase.java | 2 +- .../CheckEmailVerifiedUseCase.java | 2 +- .../{ => email}/RegisterEmailUseCase.java | 4 +- .../usecase/email/UnlinkEmailUseCase.java | 7 ++++ .../usecase/email/VerifyEmailUseCase.java | 8 ++++ .../output/RecordEmailVerifiedAtPort.java | 2 + .../port/output/SaveEmailPort.java | 3 ++ .../service/email/ChangeEmailService.java | 27 +++++++++++++ .../CheckEmailDuplicateService.java | 4 +- .../CheckEmailVerifiedService.java | 4 +- .../service/email/RegisterEmailService.java | 39 +++++++++++++++++++ .../service/email/UnlinkEmailService.java | 27 +++++++++++++ .../VerifyEmailService.java} | 18 ++++----- .../com/tune_fun/v1/common/config/Uris.java | 1 + .../v1/common/response/MessageCode.java | 1 + .../resources/messages/messages.properties | 1 + .../messages/messages_en_US.properties | 1 + .../messages/messages_ko_KR.properties | 1 + 21 files changed, 188 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/ChangeEmailUseCase.java rename src/main/java/com/tune_fun/v1/account/application/port/input/usecase/{ => email}/CheckEmailDuplicateUseCase.java (63%) rename src/main/java/com/tune_fun/v1/account/application/port/input/usecase/{ => email}/CheckEmailVerifiedUseCase.java (71%) rename src/main/java/com/tune_fun/v1/account/application/port/input/usecase/{ => email}/RegisterEmailUseCase.java (54%) create mode 100644 src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/UnlinkEmailUseCase.java create mode 100644 src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/VerifyEmailUseCase.java create mode 100644 src/main/java/com/tune_fun/v1/account/application/service/email/ChangeEmailService.java rename src/main/java/com/tune_fun/v1/account/application/service/{ => email}/CheckEmailDuplicateService.java (85%) rename src/main/java/com/tune_fun/v1/account/application/service/{ => email}/CheckEmailVerifiedService.java (88%) create mode 100644 src/main/java/com/tune_fun/v1/account/application/service/email/RegisterEmailService.java create mode 100644 src/main/java/com/tune_fun/v1/account/application/service/email/UnlinkEmailService.java rename src/main/java/com/tune_fun/v1/account/application/service/{RegisterEmailService.java => email/VerifyEmailService.java} (67%) diff --git a/src/main/java/com/tune_fun/v1/account/adapter/input/rest/EmailController.java b/src/main/java/com/tune_fun/v1/account/adapter/input/rest/EmailController.java index e1215660..a05e95e9 100644 --- a/src/main/java/com/tune_fun/v1/account/adapter/input/rest/EmailController.java +++ b/src/main/java/com/tune_fun/v1/account/adapter/input/rest/EmailController.java @@ -1,9 +1,7 @@ package com.tune_fun.v1.account.adapter.input.rest; import com.tune_fun.v1.account.application.port.input.command.AccountCommands; -import com.tune_fun.v1.account.application.port.input.usecase.CheckEmailDuplicateUseCase; -import com.tune_fun.v1.account.application.port.input.usecase.CheckEmailVerifiedUseCase; -import com.tune_fun.v1.account.application.port.input.usecase.RegisterEmailUseCase; +import com.tune_fun.v1.account.application.port.input.usecase.email.*; import com.tune_fun.v1.account.domain.value.CurrentUser; import com.tune_fun.v1.common.config.Uris; import com.tune_fun.v1.common.hexagon.WebAdapter; @@ -29,8 +27,11 @@ public class EmailController { private final CheckEmailDuplicateUseCase checkEmailDuplicateUseCase; private final CheckEmailVerifiedUseCase checkEmailVerifiedUseCase; - + private final RegisterEmailUseCase registerEmailUseCase; + private final VerifyEmailUseCase verifyEmailUseCase; + private final ChangeEmailUseCase changeEmailUseCase; + private final UnlinkEmailUseCase unlinkEmailUseCase; private final ResponseMapper responseMapper; @@ -48,23 +49,27 @@ public ResponseEntity> checkEmailVerified(@CurrentUser Use } @PostMapping(value = Uris.EMAIL_ROOT) - public ResponseEntity registerEmail(@Valid @RequestBody AccountCommands.SaveEmail command, @CurrentUser User user) { - return null; + public ResponseEntity registerEmail(@Valid @RequestBody AccountCommands.SaveEmail command, @CurrentUser User user) throws Exception { + registerEmailUseCase.registerEmail(command, user); + return responseMapper.ok(); } - @PostMapping(value = Uris.EMAIL_ROOT + "/verify") - public ResponseEntity verifyEmail(@CurrentUser User user) { - return null; + @PostMapping(value = Uris.VERIFY_EMAIL) + public ResponseEntity verifyEmail(@CurrentUser User user) throws Exception { + verifyEmailUseCase.verifyEmail(user); + return responseMapper.ok(); } @PatchMapping(value = Uris.EMAIL_ROOT) public ResponseEntity changeEmail(@Valid @RequestBody AccountCommands.SaveEmail command, @CurrentUser User user) { - return null; + changeEmailUseCase.changeEmail(command, user); + return responseMapper.ok(); } @DeleteMapping(value = Uris.EMAIL_ROOT) public ResponseEntity unlinkEmail(@CurrentUser User user) { - return null; + unlinkEmailUseCase.unlinkEmail(user); + return responseMapper.ok(); } } diff --git a/src/main/java/com/tune_fun/v1/account/adapter/output/persistence/AccountPersistenceAdapter.java b/src/main/java/com/tune_fun/v1/account/adapter/output/persistence/AccountPersistenceAdapter.java index f6c11c30..e513de18 100644 --- a/src/main/java/com/tune_fun/v1/account/adapter/output/persistence/AccountPersistenceAdapter.java +++ b/src/main/java/com/tune_fun/v1/account/adapter/output/persistence/AccountPersistenceAdapter.java @@ -26,7 +26,7 @@ public class AccountPersistenceAdapter implements LoadAccountPort, SaveAccountPort, SaveOAuth2AccountPort, DisableOAuth2AccountPort, - DeleteAccountPort, + DeleteAccountPort, SaveEmailPort, RecordLastLoginAtPort, RecordEmailVerifiedAtPort, UpdatePasswordPort, UpdateNicknamePort { @@ -73,12 +73,29 @@ public void deleteAll() { accountRepository.deleteAll(); } + @Override + public void saveEmail(final String email, final String username) { + loadAccountByUsername(username) + .ifPresent(account -> { + AccountJpaEntity updatedAccount = account.toBuilder().email(email).build(); + accountRepository.save(updatedAccount); + }); + } + + @Override + public void clearEmail(String username) { + loadAccountByUsername(username) + .ifPresent(account -> { + AccountJpaEntity updatedAccount = account.toBuilder().email(null).build(); + accountRepository.save(updatedAccount); + }); + } + @Override public void recordLastLoginAt(final String username) { loadAccountByUsername(username) .ifPresent(account -> { - AccountJpaEntity updatedAccount = account.toBuilder() - .lastLoginAt(LocalDateTime.now()).build(); + AccountJpaEntity updatedAccount = account.toBuilder().lastLoginAt(LocalDateTime.now()).build(); accountRepository.save(updatedAccount); }); } @@ -87,8 +104,16 @@ public void recordLastLoginAt(final String username) { public void recordEmailVerifiedAt(final String username) { loadAccountByUsername(username) .ifPresent(account -> { - AccountJpaEntity updatedAccount = account.toBuilder() - .emailVerifiedAt(LocalDateTime.now()).build(); + AccountJpaEntity updatedAccount = account.toBuilder().emailVerifiedAt(LocalDateTime.now()).build(); + accountRepository.save(updatedAccount); + }); + } + + @Override + public void clearEmailVerifiedAt(final String username) { + loadAccountByUsername(username) + .ifPresent(account -> { + AccountJpaEntity updatedAccount = account.toBuilder().emailVerifiedAt(null).build(); accountRepository.save(updatedAccount); }); } diff --git a/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/ChangeEmailUseCase.java b/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/ChangeEmailUseCase.java new file mode 100644 index 00000000..c2cf0c95 --- /dev/null +++ b/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/ChangeEmailUseCase.java @@ -0,0 +1,9 @@ +package com.tune_fun.v1.account.application.port.input.usecase.email; + +import com.tune_fun.v1.account.application.port.input.command.AccountCommands; +import org.springframework.security.core.userdetails.User; + +@FunctionalInterface +public interface ChangeEmailUseCase { + void changeEmail(final AccountCommands.SaveEmail command, final User user); +} diff --git a/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/CheckEmailDuplicateUseCase.java b/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/CheckEmailDuplicateUseCase.java similarity index 63% rename from src/main/java/com/tune_fun/v1/account/application/port/input/usecase/CheckEmailDuplicateUseCase.java rename to src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/CheckEmailDuplicateUseCase.java index 8579e838..457203af 100644 --- a/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/CheckEmailDuplicateUseCase.java +++ b/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/CheckEmailDuplicateUseCase.java @@ -1,4 +1,4 @@ -package com.tune_fun.v1.account.application.port.input.usecase; +package com.tune_fun.v1.account.application.port.input.usecase.email; @FunctionalInterface public interface CheckEmailDuplicateUseCase { diff --git a/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/CheckEmailVerifiedUseCase.java b/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/CheckEmailVerifiedUseCase.java similarity index 71% rename from src/main/java/com/tune_fun/v1/account/application/port/input/usecase/CheckEmailVerifiedUseCase.java rename to src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/CheckEmailVerifiedUseCase.java index 30e5485d..f16b338c 100644 --- a/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/CheckEmailVerifiedUseCase.java +++ b/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/CheckEmailVerifiedUseCase.java @@ -1,4 +1,4 @@ -package com.tune_fun.v1.account.application.port.input.usecase; +package com.tune_fun.v1.account.application.port.input.usecase.email; import org.springframework.security.core.userdetails.User; diff --git a/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/RegisterEmailUseCase.java b/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/RegisterEmailUseCase.java similarity index 54% rename from src/main/java/com/tune_fun/v1/account/application/port/input/usecase/RegisterEmailUseCase.java rename to src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/RegisterEmailUseCase.java index c6f44cf0..d6326520 100644 --- a/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/RegisterEmailUseCase.java +++ b/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/RegisterEmailUseCase.java @@ -1,4 +1,4 @@ -package com.tune_fun.v1.account.application.port.input.usecase; +package com.tune_fun.v1.account.application.port.input.usecase.email; import com.tune_fun.v1.account.application.port.input.command.AccountCommands; import org.springframework.security.core.userdetails.User; @@ -6,6 +6,6 @@ @FunctionalInterface public interface RegisterEmailUseCase { - void registerEmail(final AccountCommands.Register command, final User user) throws Exception; + void registerEmail(final AccountCommands.SaveEmail command, final User user) throws Exception; } diff --git a/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/UnlinkEmailUseCase.java b/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/UnlinkEmailUseCase.java new file mode 100644 index 00000000..f0dae2ea --- /dev/null +++ b/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/UnlinkEmailUseCase.java @@ -0,0 +1,7 @@ +package com.tune_fun.v1.account.application.port.input.usecase.email; + +import org.springframework.security.core.userdetails.User; + +public interface UnlinkEmailUseCase { + void unlinkEmail(final User user); +} diff --git a/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/VerifyEmailUseCase.java b/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/VerifyEmailUseCase.java new file mode 100644 index 00000000..9e67d02b --- /dev/null +++ b/src/main/java/com/tune_fun/v1/account/application/port/input/usecase/email/VerifyEmailUseCase.java @@ -0,0 +1,8 @@ +package com.tune_fun.v1.account.application.port.input.usecase.email; + +import org.springframework.security.core.userdetails.User; + +@FunctionalInterface +public interface VerifyEmailUseCase { + void verifyEmail(final User user) throws Exception; +} diff --git a/src/main/java/com/tune_fun/v1/account/application/port/output/RecordEmailVerifiedAtPort.java b/src/main/java/com/tune_fun/v1/account/application/port/output/RecordEmailVerifiedAtPort.java index 33fc7c59..bca4ddb2 100644 --- a/src/main/java/com/tune_fun/v1/account/application/port/output/RecordEmailVerifiedAtPort.java +++ b/src/main/java/com/tune_fun/v1/account/application/port/output/RecordEmailVerifiedAtPort.java @@ -2,4 +2,6 @@ public interface RecordEmailVerifiedAtPort { void recordEmailVerifiedAt(final String username); + + void clearEmailVerifiedAt(final String username); } diff --git a/src/main/java/com/tune_fun/v1/account/application/port/output/SaveEmailPort.java b/src/main/java/com/tune_fun/v1/account/application/port/output/SaveEmailPort.java index a43861bc..0cf74c09 100644 --- a/src/main/java/com/tune_fun/v1/account/application/port/output/SaveEmailPort.java +++ b/src/main/java/com/tune_fun/v1/account/application/port/output/SaveEmailPort.java @@ -1,4 +1,7 @@ package com.tune_fun.v1.account.application.port.output; public interface SaveEmailPort { + void saveEmail(final String email, final String username); + + void clearEmail(final String username); } diff --git a/src/main/java/com/tune_fun/v1/account/application/service/email/ChangeEmailService.java b/src/main/java/com/tune_fun/v1/account/application/service/email/ChangeEmailService.java new file mode 100644 index 00000000..ea0e3205 --- /dev/null +++ b/src/main/java/com/tune_fun/v1/account/application/service/email/ChangeEmailService.java @@ -0,0 +1,27 @@ +package com.tune_fun.v1.account.application.service.email; + +import com.tune_fun.v1.account.application.port.input.command.AccountCommands; +import com.tune_fun.v1.account.application.port.input.usecase.email.ChangeEmailUseCase; +import com.tune_fun.v1.account.application.port.output.RecordEmailVerifiedAtPort; +import com.tune_fun.v1.account.application.port.output.SaveEmailPort; +import com.tune_fun.v1.common.hexagon.UseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@UseCase +@RequiredArgsConstructor +public class ChangeEmailService implements ChangeEmailUseCase { + + private final SaveEmailPort saveEmailPort; + private final RecordEmailVerifiedAtPort recordEmailVerifiedAtPort; + + @Transactional + @Override + public void changeEmail(final AccountCommands.SaveEmail command, final User user) { + saveEmailPort.saveEmail(command.email(), user.getUsername()); + recordEmailVerifiedAtPort.clearEmailVerifiedAt(user.getUsername()); + } +} diff --git a/src/main/java/com/tune_fun/v1/account/application/service/CheckEmailDuplicateService.java b/src/main/java/com/tune_fun/v1/account/application/service/email/CheckEmailDuplicateService.java similarity index 85% rename from src/main/java/com/tune_fun/v1/account/application/service/CheckEmailDuplicateService.java rename to src/main/java/com/tune_fun/v1/account/application/service/email/CheckEmailDuplicateService.java index 47b492e4..85c70486 100644 --- a/src/main/java/com/tune_fun/v1/account/application/service/CheckEmailDuplicateService.java +++ b/src/main/java/com/tune_fun/v1/account/application/service/email/CheckEmailDuplicateService.java @@ -1,6 +1,6 @@ -package com.tune_fun.v1.account.application.service; +package com.tune_fun.v1.account.application.service.email; -import com.tune_fun.v1.account.application.port.input.usecase.CheckEmailDuplicateUseCase; +import com.tune_fun.v1.account.application.port.input.usecase.email.CheckEmailDuplicateUseCase; import com.tune_fun.v1.account.application.port.output.LoadAccountPort; import com.tune_fun.v1.common.exception.CommonApplicationException; import com.tune_fun.v1.common.hexagon.UseCase; diff --git a/src/main/java/com/tune_fun/v1/account/application/service/CheckEmailVerifiedService.java b/src/main/java/com/tune_fun/v1/account/application/service/email/CheckEmailVerifiedService.java similarity index 88% rename from src/main/java/com/tune_fun/v1/account/application/service/CheckEmailVerifiedService.java rename to src/main/java/com/tune_fun/v1/account/application/service/email/CheckEmailVerifiedService.java index 8e6abc88..533a7358 100644 --- a/src/main/java/com/tune_fun/v1/account/application/service/CheckEmailVerifiedService.java +++ b/src/main/java/com/tune_fun/v1/account/application/service/email/CheckEmailVerifiedService.java @@ -1,6 +1,6 @@ -package com.tune_fun.v1.account.application.service; +package com.tune_fun.v1.account.application.service.email; -import com.tune_fun.v1.account.application.port.input.usecase.CheckEmailVerifiedUseCase; +import com.tune_fun.v1.account.application.port.input.usecase.email.CheckEmailVerifiedUseCase; import com.tune_fun.v1.account.application.port.output.LoadAccountPort; import com.tune_fun.v1.account.domain.value.CurrentAccount; import com.tune_fun.v1.common.exception.CommonApplicationException; diff --git a/src/main/java/com/tune_fun/v1/account/application/service/email/RegisterEmailService.java b/src/main/java/com/tune_fun/v1/account/application/service/email/RegisterEmailService.java new file mode 100644 index 00000000..b8cfcde3 --- /dev/null +++ b/src/main/java/com/tune_fun/v1/account/application/service/email/RegisterEmailService.java @@ -0,0 +1,39 @@ +package com.tune_fun.v1.account.application.service.email; + +import com.tune_fun.v1.account.application.port.input.command.AccountCommands; +import com.tune_fun.v1.account.application.port.input.usecase.email.RegisterEmailUseCase; +import com.tune_fun.v1.account.application.port.output.LoadAccountPort; +import com.tune_fun.v1.account.application.port.output.SaveEmailPort; +import com.tune_fun.v1.account.domain.value.CurrentAccount; +import com.tune_fun.v1.common.exception.CommonApplicationException; +import com.tune_fun.v1.common.hexagon.UseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static com.tune_fun.v1.common.response.MessageCode.ACCOUNT_NOT_FOUND; +import static com.tune_fun.v1.common.response.MessageCode.USER_POLICY_CANNOT_REGISTER_EMAIL_TWICE; + +@Service +@UseCase +@RequiredArgsConstructor +public class RegisterEmailService implements RegisterEmailUseCase { + + private final LoadAccountPort loadAccountPort; + private final SaveEmailPort saveEmailPort; + + + @Transactional + @Override + public void registerEmail(AccountCommands.SaveEmail command, User user) throws Exception { + CurrentAccount currentAccount = loadAccountPort.currentAccountInfo(user.getUsername()) + .orElseThrow(() -> new CommonApplicationException(ACCOUNT_NOT_FOUND)); + + if (currentAccount.email() != null) + throw new CommonApplicationException(USER_POLICY_CANNOT_REGISTER_EMAIL_TWICE); + + saveEmailPort.saveEmail(command.email(), user.getUsername()); + } + +} diff --git a/src/main/java/com/tune_fun/v1/account/application/service/email/UnlinkEmailService.java b/src/main/java/com/tune_fun/v1/account/application/service/email/UnlinkEmailService.java new file mode 100644 index 00000000..ca828d59 --- /dev/null +++ b/src/main/java/com/tune_fun/v1/account/application/service/email/UnlinkEmailService.java @@ -0,0 +1,27 @@ +package com.tune_fun.v1.account.application.service.email; + +import com.tune_fun.v1.account.application.port.input.usecase.email.UnlinkEmailUseCase; +import com.tune_fun.v1.account.application.port.output.RecordEmailVerifiedAtPort; +import com.tune_fun.v1.account.application.port.output.SaveEmailPort; +import com.tune_fun.v1.common.hexagon.UseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@UseCase +@RequiredArgsConstructor +public class UnlinkEmailService implements UnlinkEmailUseCase { + + private final SaveEmailPort saveEmailPort; + private final RecordEmailVerifiedAtPort recordEmailVerifiedAtPort; + + @Transactional + @Override + public void unlinkEmail(final User user) { + saveEmailPort.clearEmail(user.getUsername()); + recordEmailVerifiedAtPort.clearEmailVerifiedAt(user.getUsername()); + } + +} diff --git a/src/main/java/com/tune_fun/v1/account/application/service/RegisterEmailService.java b/src/main/java/com/tune_fun/v1/account/application/service/email/VerifyEmailService.java similarity index 67% rename from src/main/java/com/tune_fun/v1/account/application/service/RegisterEmailService.java rename to src/main/java/com/tune_fun/v1/account/application/service/email/VerifyEmailService.java index f3a55463..d07c5402 100644 --- a/src/main/java/com/tune_fun/v1/account/application/service/RegisterEmailService.java +++ b/src/main/java/com/tune_fun/v1/account/application/service/email/VerifyEmailService.java @@ -1,9 +1,7 @@ -package com.tune_fun.v1.account.application.service; +package com.tune_fun.v1.account.application.service.email; -import com.tune_fun.v1.account.application.port.input.command.AccountCommands; -import com.tune_fun.v1.account.application.port.input.usecase.RegisterEmailUseCase; +import com.tune_fun.v1.account.application.port.input.usecase.email.VerifyEmailUseCase; import com.tune_fun.v1.account.application.port.output.LoadAccountPort; -import com.tune_fun.v1.account.application.port.output.SaveEmailPort; import com.tune_fun.v1.account.domain.value.CurrentAccount; import com.tune_fun.v1.common.exception.CommonApplicationException; import com.tune_fun.v1.common.hexagon.UseCase; @@ -19,24 +17,22 @@ import org.springframework.transaction.annotation.Transactional; import static com.tune_fun.v1.common.response.MessageCode.ACCOUNT_NOT_FOUND; -import static com.tune_fun.v1.otp.adapter.output.persistence.OtpType.FORGOT_PASSWORD; import static com.tune_fun.v1.otp.adapter.output.persistence.OtpType.VERIFY_EMAIL; @Service @UseCase @RequiredArgsConstructor -public class RegisterEmailService implements RegisterEmailUseCase { +public class VerifyEmailService implements VerifyEmailUseCase { private final LoadAccountPort loadAccountPort; - private final SaveEmailPort saveEmailPort; private final SaveOtpPort saveOtpPort; private final SendOtpPort sendOtpPort; @Transactional @Override - public void registerEmail(AccountCommands.Register command, User user) throws Exception { - CurrentAccount currentAccount = getCurrentAccount(command); + public void verifyEmail(final User user) throws Exception { + CurrentAccount currentAccount = getCurrentAccount(user.getUsername()); SaveOtp saveOtp = getSaveOtp(user.getUsername()); CurrentOtp currentOtp = saveOtpPort.saveOtp(saveOtp); @@ -51,8 +47,8 @@ private static SaveOtp getSaveOtp(String username) { } @Transactional(readOnly = true) - public CurrentAccount getCurrentAccount(final AccountCommands.Register command) { - return loadAccountPort.currentAccountInfo(command.username()) + public CurrentAccount getCurrentAccount(final String username) { + return loadAccountPort.currentAccountInfo(username) .orElseThrow(() -> new CommonApplicationException(ACCOUNT_NOT_FOUND)); } } diff --git a/src/main/java/com/tune_fun/v1/common/config/Uris.java b/src/main/java/com/tune_fun/v1/common/config/Uris.java index ade7f41f..2250412c 100644 --- a/src/main/java/com/tune_fun/v1/common/config/Uris.java +++ b/src/main/java/com/tune_fun/v1/common/config/Uris.java @@ -50,6 +50,7 @@ private Uris() { public static final String CHECK_EMAIL_VERIFIED = API_V1_ROOT + "/check-email-verified"; public static final String EMAIL_ROOT = API_V1_ROOT + "/accounts/email"; + public static final String VERIFY_EMAIL = EMAIL_ROOT + "/verify"; public static final String VERIFY_OTP = API_V1_ROOT + "/otp/verify"; public static final String VERIFY_OTP_WITH_AUTHORIZATION = API_V1_ROOT + "/otp/verify-with-authorization"; diff --git a/src/main/java/com/tune_fun/v1/common/response/MessageCode.java b/src/main/java/com/tune_fun/v1/common/response/MessageCode.java index 2cd4a859..cf0d5542 100644 --- a/src/main/java/com/tune_fun/v1/common/response/MessageCode.java +++ b/src/main/java/com/tune_fun/v1/common/response/MessageCode.java @@ -42,6 +42,7 @@ public enum MessageCode { SUCCESS_UPDATE_NICKNAME(OK, "2011"), USER_POLICY_CANNOT_UNLINK_UNIQUE_PROVIDER(BAD_REQUEST, "2012"), USER_POLICY_ALREADY_LINKED_PROVIDER(BAD_REQUEST, "2013"), + USER_POLICY_CANNOT_REGISTER_EMAIL_TWICE(BAD_REQUEST, "2014"), // 로그인 관련 EXCEPTION_AUTHENTICATION_LOGIN_FAIL(UNAUTHORIZED, "3001"), diff --git a/src/main/resources/messages/messages.properties b/src/main/resources/messages/messages.properties index 72f74f74..eaecabc6 100644 --- a/src/main/resources/messages/messages.properties +++ b/src/main/resources/messages/messages.properties @@ -20,6 +20,7 @@ 2011=\uB2C9\uB124\uC784 \uBCC0\uACBD\uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. 2012=\uCCAB\uBC88\uC9F8\uB85C \uC5F0\uACB0\uD55C \uC18C\uC15C \uACC4\uC815\uC740 \uC5F0\uACB0\uD574\uC81C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. 2013=\uC774\uBBF8 \uC5F0\uACB0\uB41C \uC18C\uC15C \uACC4\uC815\uC785\uB2C8\uB2E4. +2014=2\uD68C \uC5F0\uC18D\uC73C\uB85C \uC774\uBA54\uC77C \uB4F1\uB85D\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. 3001=\uB85C\uADF8\uC778\uC5D0 \uC2E4\uD328\uD558\uC600\uC2B5\uB2C8\uB2E4. 3002=\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uC778\uC99D \uC815\uBCF4\uC785\uB2C8\uB2E4. 3003=\uC778\uC99D \uC815\uBCF4\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. diff --git a/src/main/resources/messages/messages_en_US.properties b/src/main/resources/messages/messages_en_US.properties index ac514932..35f25dcc 100644 --- a/src/main/resources/messages/messages_en_US.properties +++ b/src/main/resources/messages/messages_en_US.properties @@ -20,6 +20,7 @@ 2011=Nickname change has been completed. 2012=Cannot unlink the first linked account. 2013=Already linked account. +2014=You can't sign up for email twice in a row. 3001=Login failed. 3002=Invalid credentials. 3003=Credentials not found. diff --git a/src/main/resources/messages/messages_ko_KR.properties b/src/main/resources/messages/messages_ko_KR.properties index 72f74f74..eaecabc6 100644 --- a/src/main/resources/messages/messages_ko_KR.properties +++ b/src/main/resources/messages/messages_ko_KR.properties @@ -20,6 +20,7 @@ 2011=\uB2C9\uB124\uC784 \uBCC0\uACBD\uC774 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. 2012=\uCCAB\uBC88\uC9F8\uB85C \uC5F0\uACB0\uD55C \uC18C\uC15C \uACC4\uC815\uC740 \uC5F0\uACB0\uD574\uC81C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. 2013=\uC774\uBBF8 \uC5F0\uACB0\uB41C \uC18C\uC15C \uACC4\uC815\uC785\uB2C8\uB2E4. +2014=2\uD68C \uC5F0\uC18D\uC73C\uB85C \uC774\uBA54\uC77C \uB4F1\uB85D\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. 3001=\uB85C\uADF8\uC778\uC5D0 \uC2E4\uD328\uD558\uC600\uC2B5\uB2C8\uB2E4. 3002=\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uC778\uC99D \uC815\uBCF4\uC785\uB2C8\uB2E4. 3003=\uC778\uC99D \uC815\uBCF4\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. From 3681d511a8286ff51422774a0921bf1352ef78b9 Mon Sep 17 00:00:00 2001 From: habin Date: Sat, 1 Jun 2024 15:55:47 +0900 Subject: [PATCH 6/6] feat(account) email MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test: 이메일 등록 API 통합 테스트 추가 test: 이메일 인증번호 발송 API 통합 테스트 추가 test: 이메일 변경 API 통합 테스트 추가 test: 이메일 해제 API 통합 테스트 추가 docs: Email --- src/docs/asciidoc/account-api.adoc | 54 ++++- .../output/persistence/AccountJpaEntity.java | 1 - .../adapter/input/rest/EmailControllerIT.java | 219 +++++++++++++++++- .../com/tune_fun/v1/dummy/DummyService.java | 28 ++- 4 files changed, 277 insertions(+), 25 deletions(-) diff --git a/src/docs/asciidoc/account-api.adoc b/src/docs/asciidoc/account-api.adoc index 3a9857e2..b08124a4 100644 --- a/src/docs/asciidoc/account-api.adoc +++ b/src/docs/asciidoc/account-api.adoc @@ -25,12 +25,6 @@ operation::logout-success[snippets='http-request,http-response,request-fields,re operation::refresh-success[snippets='http-request,http-response,request-fields,response-fields'] -=== 이메일 중복확인 API - -==== 성공 - -operation::check-email-duplicate-success[snippets='http-request,http-response,query-parameters,response-fields'] - ==== 실패 operation::check-email-duplicate-failed[snippets='http-request,http-response,query-parameters,response-fields'] @@ -45,26 +39,62 @@ operation::check-username-duplicate-success[snippets='http-request,http-response operation::check-username-duplicate-success-failed[snippets='http-request,http-response'] +=== 비밀번호 찾기 OTP 전송 API + +==== 성공 + +operation::send-forgot-password-otp-success[snippets='http-request,http-response,request-fields,response-fields'] + +=== 비밀번호 재설정 API + +==== 성공 + +operation::set-new-password-success[snippets='http-request,http-response,request-fields,response-fields'] + +=== 닉네임 변경 API + +==== 성공 + +operation::update-nickname-success[snippets='http-request,http-response,request-fields,response-fields' + +== Email API + +=== 이메일 중복확인 API + +==== 성공 + +operation::check-email-duplicate-success[snippets='http-request,http-response,query-parameters,response-fields'] + +==== 실패 + +operation::check-email-duplicate-failed[snippets='http-request,http-response,query-parameters,response-fields'] + === 이메일 인증여부 확인 API ==== 성공 operation::check-email-verified-success[snippets='http-request,http-response,response-fields'] -=== 비밀번호 찾기 OTP 전송 API +=== 이메일 등록 API ==== 성공 -operation::send-forgot-password-otp-success[snippets='http-request,http-response,request-fields,response-fields'] +operation::register-email-success[snippets='http-request,http-response,request-fields,response-fields'] -=== 비밀번호 재설정 API +=== 이메일 인증번호 발송 API ==== 성공 -operation::set-new-password-success[snippets='http-request,http-response,request-fields,response-fields'] +operation::verify-email-success[snippets='http-request,http-response,response-fields'] -=== 닉네임 변경 API +=== 이메일 변경 API + +==== 성공 + +operation::change-email-success[snippets='http-request,http-response,request-fields,response-fields'] + +=== 이메일 해제 API ==== 성공 -operation::update-nickname-success[snippets='http-request,http-response,request-fields,response-fields'] +operation::unlink-email-success[snippets='http-request,http-response,response-fields'] \ No newline at end of file diff --git a/src/main/java/com/tune_fun/v1/account/adapter/output/persistence/AccountJpaEntity.java b/src/main/java/com/tune_fun/v1/account/adapter/output/persistence/AccountJpaEntity.java index 8127d428..5aecf6f3 100644 --- a/src/main/java/com/tune_fun/v1/account/adapter/output/persistence/AccountJpaEntity.java +++ b/src/main/java/com/tune_fun/v1/account/adapter/output/persistence/AccountJpaEntity.java @@ -51,7 +51,6 @@ public class AccountJpaEntity extends BaseEntity implements UserDetails { @Comment("비밀번호") private String password; - @NotNull @Column(name = "email", length = 2000) @Comment("이메일") private String email; diff --git a/src/test/java/com/tune_fun/v1/account/adapter/input/rest/EmailControllerIT.java b/src/test/java/com/tune_fun/v1/account/adapter/input/rest/EmailControllerIT.java index 2e3d892c..2fdb0a6c 100644 --- a/src/test/java/com/tune_fun/v1/account/adapter/input/rest/EmailControllerIT.java +++ b/src/test/java/com/tune_fun/v1/account/adapter/input/rest/EmailControllerIT.java @@ -1,27 +1,43 @@ package com.tune_fun.v1.account.adapter.input.rest; +import com.icegreen.greenmail.util.GreenMail; import com.tune_fun.v1.account.adapter.output.persistence.AccountJpaEntity; +import com.tune_fun.v1.account.application.port.input.command.AccountCommands; +import com.tune_fun.v1.account.application.port.output.LoadAccountPort; import com.tune_fun.v1.base.ControllerBaseTest; import com.tune_fun.v1.common.config.Uris; import com.tune_fun.v1.common.response.MessageCode; +import com.tune_fun.v1.common.util.StringUtil; import com.tune_fun.v1.dummy.DummyService; import com.tune_fun.v1.otp.adapter.output.persistence.OtpType; +import com.tune_fun.v1.otp.application.port.output.LoadOtpPort; +import com.tune_fun.v1.otp.application.port.output.VerifyOtpPort; +import com.tune_fun.v1.otp.domain.behavior.LoadOtp; +import com.tune_fun.v1.otp.domain.behavior.VerifyOtp; import com.tune_fun.v1.otp.domain.value.CurrentDecryptedOtp; +import jakarta.mail.internet.MimeMessage; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junitpioneer.jupiter.Issue; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.request.ParameterDescriptor; +import org.springframework.test.web.servlet.ResultActions; import org.springframework.transaction.annotation.Transactional; import static com.epages.restdocs.apispec.ResourceDocumentation.resource; import static com.epages.restdocs.apispec.ResourceSnippetParameters.builder; import static com.tune_fun.v1.base.doc.RestDocsConfig.constraint; +import static com.tune_fun.v1.otp.adapter.output.persistence.OtpType.VERIFY_EMAIL; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; -import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; @@ -30,8 +46,17 @@ class EmailControllerIT extends ControllerBaseTest { @Autowired private DummyService dummyService; - private static final ParameterDescriptor REQUEST_DESCRIPTOR = parameterWithName("email").description("이메일") - .attributes(constraint("NOT BLANK")); + @Autowired + private LoadAccountPort loadAccountPort; + + @Autowired + private LoadOtpPort loadOtpPort; + + @Autowired + private VerifyOtpPort verifyOtpPort; + + @Autowired + private GreenMail greenMail; @Transactional @Test @@ -41,6 +66,9 @@ class EmailControllerIT extends ControllerBaseTest { void checkEmailDuplicateSuccess() throws Exception { dummyService.initAccount(); + ParameterDescriptor requestDescriptor = parameterWithName("email").description("이메일") + .attributes(constraint("NOT BLANK")); + mockMvc.perform( get(Uris.CHECK_EMAIL_DUPLICATE) .param("email", "test") @@ -48,12 +76,12 @@ void checkEmailDuplicateSuccess() throws Exception { .andExpectAll(baseAssertion(MessageCode.SUCCESS_EMAIL_UNIQUE)) .andDo( restDocs.document( - queryParameters(REQUEST_DESCRIPTOR), + queryParameters(requestDescriptor), responseFields(baseResponseFields), resource( builder(). description("이메일 중복확인"). - queryParameters(REQUEST_DESCRIPTOR). + queryParameters(requestDescriptor). responseFields(baseResponseFields) .build() ) @@ -70,6 +98,9 @@ void checkEmailDuplicateFailed() throws Exception { dummyService.initAccount(); AccountJpaEntity account = dummyService.getDefaultAccount(); + ParameterDescriptor requestDescriptor = parameterWithName("email").description("이메일") + .attributes(constraint("NOT BLANK")); + mockMvc.perform( get(Uris.CHECK_EMAIL_DUPLICATE) .param("email", account.getEmail()) @@ -77,12 +108,12 @@ void checkEmailDuplicateFailed() throws Exception { .andExpectAll(baseAssertion(MessageCode.USER_POLICY_EMAIL_REGISTERED)) .andDo( restDocs.document( - queryParameters(REQUEST_DESCRIPTOR), + queryParameters(requestDescriptor), responseFields(baseResponseFields), resource( builder(). description("이메일 중복확인"). - queryParameters(REQUEST_DESCRIPTOR). + queryParameters(requestDescriptor). responseFields(baseResponseFields) .build() ) @@ -125,4 +156,176 @@ void checkEmailVerifiedSuccess() throws Exception { } + @Transactional + @Test + @Order(4) + @DisplayName("이메일 등록, 성공") + void registerEmailSuccess() throws Exception { + dummyService.initAndLogin(); + dummyService.clearEmail(); + + String accessToken = dummyService.getDefaultAccessToken(); + + String email = StringUtil.randomAlphabetic(7) + "@" + StringUtil.randomAlphabetic(5) + ".com"; + AccountCommands.SaveEmail command = new AccountCommands.SaveEmail(email); + + FieldDescriptor requestDescriptor = fieldWithPath("email").description("이메일").attributes(constraint("NOT BLANK")); + + mockMvc.perform( + post(Uris.EMAIL_ROOT) + .header(AUTHORIZATION, bearerToken(accessToken)) + .content(toJson(command)) + .contentType(APPLICATION_JSON_VALUE) + ) + .andExpectAll(baseAssertion(MessageCode.SUCCESS)) + .andDo( + restDocs.document( + requestHeaders(authorizationHeader), + requestFields(requestDescriptor), + responseFields(baseResponseFields), + resource( + builder(). + description("이메일 등록"). + requestHeaders(authorizationHeader). + requestFields(requestDescriptor). + responseFields(baseResponseFields) + .build() + ) + ) + ); + + loadAccountPort.currentAccountInfo(dummyService.getDefaultAccount().getUsername()) + .ifPresentOrElse( + account -> assertEquals(email, account.email()), + () -> Assertions.fail("계정 정보를 찾을 수 없습니다.") + ); + } + + @Transactional + @Test + @Order(5) + @DisplayName("이메일 인증번호 발송, 성공") + void verifyEmailSuccess() throws Exception { + dummyService.initAndLogin(); + AccountJpaEntity defaultAccount = dummyService.getDefaultAccount(); + String accessToken = dummyService.getDefaultAccessToken(); + + greenMail.purgeEmailFromAllMailboxes(); + + String username = dummyService.getDefaultUsername(); + + ResultActions resultActions = mockMvc.perform( + post(Uris.VERIFY_EMAIL) + .header(AUTHORIZATION, bearerToken(accessToken)) + ) + .andExpectAll(baseAssertion(MessageCode.SUCCESS)); + + + greenMail.waitForIncomingEmail(1); + + MimeMessage receivedMessage = greenMail.getReceivedMessages()[0]; + assertEquals(defaultAccount.getEmail(), receivedMessage.getAllRecipients()[0].toString()); + assertEquals("TuneFun - " + defaultAccount.getNickname() + "님의 인증번호입니다.", receivedMessage.getSubject()); + + LoadOtp loadOtpBehavior = new LoadOtp(username, VERIFY_EMAIL.getLabel()); + CurrentDecryptedOtp decryptedOtp = loadOtpPort.loadOtp(loadOtpBehavior); + + VerifyOtp verifyOtpBehavior = new VerifyOtp(username, VERIFY_EMAIL.getLabel(), decryptedOtp.token()); + assertDoesNotThrow(() -> verifyOtpPort.verifyOtp(verifyOtpBehavior)); + + resultActions.andDo( + restDocs.document( + requestHeaders(authorizationHeader), + responseFields(baseResponseFields), + resource( + builder(). + description("이메일 인증번호 발송"). + requestHeaders(authorizationHeader). + responseFields(baseResponseFields) + .build() + ) + ) + ); + } + + @Transactional + @Test + @Order(6) + @DisplayName("이메일 변경, 성공") + void changeEmailSuccess() throws Exception { + dummyService.initAndLogin(); + + String accessToken = dummyService.getDefaultAccessToken(); + + String email = StringUtil.randomAlphabetic(7) + "@" + StringUtil.randomAlphabetic(5) + ".com"; + AccountCommands.SaveEmail command = new AccountCommands.SaveEmail(email); + + FieldDescriptor requestDescriptor = fieldWithPath("email").description("이메일").attributes(constraint("NOT BLANK")); + + mockMvc.perform( + patch(Uris.EMAIL_ROOT) + .header(AUTHORIZATION, bearerToken(accessToken)) + .content(toJson(command)) + .contentType(APPLICATION_JSON_VALUE) + ) + .andExpectAll(baseAssertion(MessageCode.SUCCESS)) + .andDo( + restDocs.document( + requestHeaders(authorizationHeader), + requestFields(requestDescriptor), + responseFields(baseResponseFields), + resource( + builder(). + description("이메일 변경"). + requestHeaders(authorizationHeader). + requestFields(requestDescriptor). + responseFields(baseResponseFields) + .build() + ) + ) + ); + + loadAccountPort.currentAccountInfo(dummyService.getDefaultAccount().getUsername()) + .ifPresentOrElse( + account -> assertEquals(email, account.email()), + () -> Assertions.fail("계정 정보를 찾을 수 없습니다.") + ); + } + + @Transactional + @Test + @Order(7) + @DisplayName("이메일 해제, 성공") + void unlinkEmailSuccess() throws Exception { + dummyService.initAndLogin(); + + String accessToken = dummyService.getDefaultAccessToken(); + + mockMvc.perform( + delete(Uris.EMAIL_ROOT) + .header(AUTHORIZATION, bearerToken(accessToken)) + .contentType(APPLICATION_JSON_VALUE) + ) + .andExpectAll(baseAssertion(MessageCode.SUCCESS)) + .andDo( + restDocs.document( + requestHeaders(authorizationHeader), + responseFields(baseResponseFields), + resource( + builder(). + description("이메일 해제"). + requestHeaders(authorizationHeader). + responseFields(baseResponseFields) + .build() + ) + ) + ); + + loadAccountPort.currentAccountInfo(dummyService.getDefaultAccount().getUsername()) + .ifPresentOrElse( + account -> Assertions.assertNull(account.email()), + () -> Assertions.fail("계정 정보를 찾을 수 없습니다.") + ); + } + } diff --git a/src/test/java/com/tune_fun/v1/dummy/DummyService.java b/src/test/java/com/tune_fun/v1/dummy/DummyService.java index 9de0c433..cba42683 100644 --- a/src/test/java/com/tune_fun/v1/dummy/DummyService.java +++ b/src/test/java/com/tune_fun/v1/dummy/DummyService.java @@ -8,6 +8,7 @@ import com.tune_fun.v1.account.application.port.input.command.AccountCommands; import com.tune_fun.v1.account.application.port.input.usecase.RegisterUseCase; import com.tune_fun.v1.account.application.port.input.usecase.SendForgotPasswordOtpUseCase; +import com.tune_fun.v1.account.application.port.input.usecase.email.RegisterEmailUseCase; import com.tune_fun.v1.account.application.port.input.usecase.jwt.GenerateAccessTokenUseCase; import com.tune_fun.v1.account.application.port.input.usecase.jwt.GenerateRefreshTokenUseCase; import com.tune_fun.v1.account.domain.behavior.SaveDevice; @@ -72,6 +73,9 @@ public class DummyService { @Autowired private SendForgotPasswordOtpUseCase sendForgotPasswordOtpUseCase; + @Autowired + private RegisterEmailUseCase registerEmailUseCase; + @Autowired private VerifyOtpUseCase verifyOtpUseCase; @@ -203,6 +207,19 @@ public void loginArtist(final AccountJpaEntity account) throws NoSuchAlgorithmEx SecurityContextHolder.getContext().setAuthentication(authentication); } + @Transactional + public void clearEmail() throws Exception { + accountPersistenceAdapter.clearEmail(defaultUsername); + } + + @Transactional + public void registerEmail() throws Exception { + String email = StringUtil.randomAlphabetic(7) + "@" + StringUtil.randomAlphabetic(5) + ".com"; + AccountCommands.SaveEmail command = new AccountCommands.SaveEmail(email); + + registerEmailUseCase.registerEmail(command, getSecurityUser(defaultAccount)); + } + @Transactional public void forgotPasswordOtp() throws Exception { AccountCommands.SendForgotPasswordOtp command = new AccountCommands.SendForgotPasswordOtp(defaultUsername); @@ -232,7 +249,7 @@ public void initVotePaper() { VotePaperCommands.Register command = new VotePaperCommands.Register("First Vote Paper", "test", VotePaperOption.DENY_ADD_CHOICES, voteStartAt, voteEndAt, offers); - User user = new User(defaultArtistUsername, defaultArtistPassword, defaultArtistAccount.getAuthorities()); + User user = getSecurityUser(defaultArtistAccount); RegisteredVotePaper registeredVotePaper = saveVotePaper(command, user); saveVoteChoiceByRegisteredVotePaper(command, registeredVotePaper); @@ -257,7 +274,7 @@ public void initVotePaperAllowAddChoices() { VotePaperCommands.Register command = new VotePaperCommands.Register("First Vote Paper", "test", VotePaperOption.ALLOW_ADD_CHOICES, voteStartAt, voteEndAt, offers); - User user = new User(defaultArtistUsername, defaultArtistPassword, defaultArtistAccount.getAuthorities()); + User user = getSecurityUser(defaultArtistAccount); RegisteredVotePaper registeredVotePaper = saveVotePaper(command, user); saveVoteChoiceByRegisteredVotePaper(command, registeredVotePaper); @@ -301,8 +318,7 @@ public int getBatchSize() { } public void registerVote() { - User user = new User(defaultUsername, defaultPassword, defaultAccount.getAuthorities()); - registerVoteUseCase.register(defaultVotePaper.getId(), defaultVoteChoices.get(0).getId(), user); + registerVoteUseCase.register(defaultVotePaper.getId(), defaultVoteChoices.get(0).getId(), getSecurityUser(defaultAccount)); } public void expireOtp(OtpType otpType) { @@ -326,4 +342,8 @@ public void likeVotePaper(final Long votePaperId, final String username) { votePersistenceAdapter.saveVotePaperLike(votePaperId, username); likeCountPersistenceAdapter.incrementVotePaperLikeCount(votePaperId); } + + private static User getSecurityUser(AccountJpaEntity account) { + return new User(account.getUsername(), account.getPassword(), account.getAuthorities()); + } }