diff --git a/src/docs/asciidoc/account-api.adoc b/src/docs/asciidoc/account-api.adoc index 3a9857e..b08124a 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 8127d42..5aecf6f 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 2e3d892..2fdb0a6 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 9de0c43..cba4268 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()); + } }