From cf8baf209b5abff199ac90b50f5e4a7be666df38 Mon Sep 17 00:00:00 2001 From: Do Yeop Kim <113661364+Dobby-Kim@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:40:30 +0900 Subject: [PATCH] Release v1.2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix-be: applicantCard 조회 시 평가 점수와 갯수가 모든 프로세스에 대해 나오는 버그 수정 (#902) fix-be: 이메일 인증 시 이미 가입된 이메일은 예외를 던지도록 변경 (#906) feat-be: RTR 방식 Redis로 변경 (#889) feat-be: 이메일 전송 템플릿 적용 (#897) feat-be: 이메일 인증 후, 회원가입 이메일의 인증 여부 확인 (#882) refactor-be: 식별자에 Tsid 적용 (#856) feat-be: 이메일 전송 로직 중 지원자 일괄 조회 로직 구현 (#869) fix-be: github action의 Redis 환경 변수 추가 (#871) feat-be: 이메일 인증 기능 구현 (#848) --- .github/workflows/be-cd_dev-docker.yml | 1 + backend/build.gradle | 1 + backend/docker-compose.dev.yml | 14 ++ backend/src/docs/asciidoc/applyform.adoc | 6 +- backend/src/docs/asciidoc/email.adoc | 32 ++++ backend/src/docs/asciidoc/member.adoc | 4 + backend/src/docs/asciidoc/question.adoc | 2 +- .../repository/ApplicantRepository.java | 6 +- .../applicant/service/ApplicantService.java | 12 +- .../controller/ApplyFormController.java | 4 +- .../com/cruru/applyform/domain/ApplyForm.java | 6 +- .../com/cruru/auth/domain/RefreshToken.java | 42 +---- .../repository/RefreshTokenRepository.java | 22 --- .../com/cruru/auth/facade/AuthFacade.java | 4 +- .../com/cruru/auth/service/AuthService.java | 66 ++++--- .../cruru/auth/service/TokenRedisClient.java | 41 +++++ .../com/cruru/config/DataSourceConfig.java | 38 ++-- .../java/com/cruru/config/WebMvcConfig.java | 2 + .../response/DashboardCreateResponse.java | 5 +- .../response/DashboardPreviewResponse.java | 12 +- .../dashboard/facade/DashboardFacade.java | 4 +- .../email/controller/EmailController.java | 19 +- .../{dto => request}/EmailRequest.java | 2 +- .../request/SendVerificationCodeRequest.java | 12 ++ .../controller/request/VerifyCodeRequest.java | 15 ++ .../exception/EmailConflictException.java | 12 ++ .../exception/NotVerifiedEmailException.java | 13 ++ .../VerificationCodeMismatchException.java | 12 ++ .../VerificationCodeNotFoundException.java | 12 ++ .../com/cruru/email/facade/EmailFacade.java | 38 +++- .../cruru/email/service/EmailRedisClient.java | 55 ++++++ .../com/cruru/email/service/EmailService.java | 24 ++- .../email/service/VerificationStatus.java | 29 +++ .../com/cruru/email/util/EmailTemplate.java | 52 ++++++ .../email/util/VerificationCodeUtil.java | 28 +++ .../com/cruru/global/util/TsidSupplier.java | 14 ++ .../com/cruru/member/facade/MemberFacade.java | 3 + .../cruru/member/service/MemberService.java | 4 + .../controller/response/ProcessResponses.java | 11 +- .../cruru/process/facade/ProcessFacade.java | 2 +- backend/src/main/resources/application.yml | 6 +- .../migration/V2_1_5__delete_token_table.sql | 8 + .../repository/ApplicantRepositoryTest.java | 56 +++--- .../service/ApplicantServiceTest.java | 35 +++- .../controller/ApplyFormControllerTest.java | 12 +- .../repository/ApplyFormRepositoryTest.java | 2 +- .../applyform/facade/ApplyFormFacadeTest.java | 2 +- .../auth/aspect/AuthValidationAspectTest.java | 3 +- .../auth/controller/AuthControllerTest.java | 10 +- .../RefreshTokenRepositoryTest.java | 66 ------- .../cruru/auth/service/AuthServiceTest.java | 24 +-- .../dashboard/facade/DashboardFacadeTest.java | 2 +- .../email/controller/EmailControllerTest.java | 167 ++++++++++++++++++ .../cruru/email/facade/EmailFacadeTest.java | 84 ++++++++- .../controller/MemberControllerTest.java | 37 +++- .../cruru/member/facade/MemberFacadeTest.java | 26 +++ .../process/facade/ProcessFacadeTest.java | 14 +- .../controller/QuestionControllerTest.java | 6 +- .../question/facade/QuestionFacadeTest.java | 47 ++++- .../java/com/cruru/util/ControllerTest.java | 2 +- 60 files changed, 1003 insertions(+), 287 deletions(-) delete mode 100644 backend/src/main/java/com/cruru/auth/domain/repository/RefreshTokenRepository.java create mode 100644 backend/src/main/java/com/cruru/auth/service/TokenRedisClient.java rename backend/src/main/java/com/cruru/email/controller/{dto => request}/EmailRequest.java (95%) create mode 100644 backend/src/main/java/com/cruru/email/controller/request/SendVerificationCodeRequest.java create mode 100644 backend/src/main/java/com/cruru/email/controller/request/VerifyCodeRequest.java create mode 100644 backend/src/main/java/com/cruru/email/exception/EmailConflictException.java create mode 100644 backend/src/main/java/com/cruru/email/exception/NotVerifiedEmailException.java create mode 100644 backend/src/main/java/com/cruru/email/exception/badrequest/VerificationCodeMismatchException.java create mode 100644 backend/src/main/java/com/cruru/email/exception/badrequest/VerificationCodeNotFoundException.java create mode 100644 backend/src/main/java/com/cruru/email/service/EmailRedisClient.java create mode 100644 backend/src/main/java/com/cruru/email/service/VerificationStatus.java create mode 100644 backend/src/main/java/com/cruru/email/util/EmailTemplate.java create mode 100644 backend/src/main/java/com/cruru/email/util/VerificationCodeUtil.java create mode 100644 backend/src/main/java/com/cruru/global/util/TsidSupplier.java create mode 100644 backend/src/main/resources/db/migration/V2_1_5__delete_token_table.sql delete mode 100644 backend/src/test/java/com/cruru/auth/domain/repository/RefreshTokenRepositoryTest.java diff --git a/.github/workflows/be-cd_dev-docker.yml b/.github/workflows/be-cd_dev-docker.yml index 8de997a71..184ed58bc 100644 --- a/.github/workflows/be-cd_dev-docker.yml +++ b/.github/workflows/be-cd_dev-docker.yml @@ -135,6 +135,7 @@ jobs: REDIS_PORT=${{ secrets.REDIS_PORT }} REDIS_HOST=${{ secrets.REDIS_HOST }} REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }} + REDIS_IP_ADDRESS=${{ secrets.REDIS_IP_ADDRESS }} EOF # - name: Check if MySQL container is running diff --git a/backend/build.gradle b/backend/build.gradle index c5a1b2bb3..4af5fcee3 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -40,6 +40,7 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' implementation 'org.flywaydb:flyway-core:9.22.3' implementation 'org.flywaydb:flyway-mysql' + implementation 'io.hypersistence:hypersistence-utils-hibernate-60:3.5.2' // Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' diff --git a/backend/docker-compose.dev.yml b/backend/docker-compose.dev.yml index 270764bbd..161f1ce86 100644 --- a/backend/docker-compose.dev.yml +++ b/backend/docker-compose.dev.yml @@ -23,6 +23,7 @@ services: platform: linux/arm64 depends_on: - database-mysql + - redis restart: always image: ${DOCKER_REPO_NAME}/cruru:${DOCKER_IMAGE_VERSION_TAG} ports: @@ -39,6 +40,19 @@ services: cruru_network: ipv4_address: ${APP_IP_ADDRESS} + redis: + container_name: redis-container + image: redis:latest + environment: + TZ: Asia/Seoul + REDIS_PASSWORD: ${REDIS_PASSWORD} + ports: + - ${REDIS_PORT}:6379 + command: [ "redis-server", "--requirepass", "${REDIS_PASSWORD}" ] + networks: + cruru_network: + ipv4_address: ${REDIS_IP_ADDRESS} + promtail: environment: TZ: Asia/Seoul diff --git a/backend/src/docs/asciidoc/applyform.adoc b/backend/src/docs/asciidoc/applyform.adoc index 6e6000f46..7ddc583ab 100644 --- a/backend/src/docs/asciidoc/applyform.adoc +++ b/backend/src/docs/asciidoc/applyform.adoc @@ -4,7 +4,7 @@ ==== 성공 -operation::applyform/submit[snippets="http-request,path-parameters,request-fields,http-response"] +operation::applyform/submit-id[snippets="http-request,path-parameters,request-fields,http-response"] ==== 실패: 개인정보 활용 거부 @@ -42,7 +42,7 @@ operation::applicant/submit-fail/required-not-replied[snippets="http-request,pat ==== 성공 -operation::applicant/read-applyform[snippets="http-request,path-parameters,http-response,response-fields"] +operation::applicant/read-applyform-id[snippets="http-request,path-parameters,http-response,response-fields"] ==== 실패: 존재하지 않는 지원폼 @@ -52,7 +52,7 @@ operation::applicant/read-applyform-fail/applyform-not-found[snippets="http-requ ==== 성공 -operation::applicant/update[snippets="http-request,request-cookies,path-parameters,request-fields,http-response"] +operation::applicant/update-id[snippets="http-request,request-cookies,path-parameters,request-fields,http-response"] ==== 실패: 존재하지 않는 지원폼 diff --git a/backend/src/docs/asciidoc/email.adoc b/backend/src/docs/asciidoc/email.adoc index ea8f7179d..3bb3f2f32 100644 --- a/backend/src/docs/asciidoc/email.adoc +++ b/backend/src/docs/asciidoc/email.adoc @@ -13,3 +13,35 @@ operation::email/send-fail/invalid-email[snippets="http-request,request-cookies, ==== 실패: 존재하지 않는 동아리 operation::email/send-fail/club-not-found[snippets="http-request,request-cookies,request-parts,http-response"] + +=== 이메일 인증 번호 발송 + +==== 성공 + +operation::email/verification-code[snippets="http-request,request-fields,http-response"] + +==== 실패: 이메일 형식이 올바르지 않은 이메일 형식 + +operation::email/verification-code-fail/invalid-email[snippets="http-request,request-fields,http-response"] + +==== 실패: 이미 가입된 이메일 + +operation::email/verification-code-fail/already-signed-up[snippets="http-request,request-fields,http-response"] + +=== 이메일 인증 확인 + +==== 성공 + +operation::email/verify-code[snippets="http-request,request-fields,http-response"] + +==== 실패: 이메일 형식이 올바르지 않은 이메일 형식 + +operation::email/verify-code-fail/invalid-email[snippets="http-request,request-fields,http-response"] + +==== 실패: 인증 번호가 없는 이메일 + +operation::email/verify-code-fail/code-not-found[snippets="http-request,request-fields,http-response"] + +==== 실패: 인증 번호가 다른 이메일 + +operation::email/verify-code-fail/code-mismatch[snippets="http-request,request-fields,http-response"] diff --git a/backend/src/docs/asciidoc/member.adoc b/backend/src/docs/asciidoc/member.adoc index fbd907607..ba0393b3f 100644 --- a/backend/src/docs/asciidoc/member.adoc +++ b/backend/src/docs/asciidoc/member.adoc @@ -9,3 +9,7 @@ operation::member/signup[snippets="http-request,request-fields,http-response"] ==== 실패: 유효하지 않은 요청 operation::member/signup-fail/invalid-request[snippets="http-request,request-fields,http-response"] + +==== 실패: 인증되지 않은 이메일 + +operation::member/signup-fail/not-verified-email[snippets="http-request,request-fields,http-response"] diff --git a/backend/src/docs/asciidoc/question.adoc b/backend/src/docs/asciidoc/question.adoc index e6896c0e7..36bc2d90c 100644 --- a/backend/src/docs/asciidoc/question.adoc +++ b/backend/src/docs/asciidoc/question.adoc @@ -4,7 +4,7 @@ ==== 성공 -operation::question/update[snippets="http-request,request-cookies,query-parameters,request-fields,http-response"] +operation::question/update-id[snippets="http-request,request-cookies,query-parameters,request-fields,http-response"] ==== 실패: 존재하지 않는 지원폼 diff --git a/backend/src/main/java/com/cruru/applicant/domain/repository/ApplicantRepository.java b/backend/src/main/java/com/cruru/applicant/domain/repository/ApplicantRepository.java index 7545cf1c1..6bcb3f0f5 100644 --- a/backend/src/main/java/com/cruru/applicant/domain/repository/ApplicantRepository.java +++ b/backend/src/main/java/com/cruru/applicant/domain/repository/ApplicantRepository.java @@ -24,11 +24,11 @@ public interface ApplicantRepository extends JpaRepository { long countByProcess(Process process); @Query(""" - SELECT new com.cruru.applicant.domain.dto.ApplicantCard( + SELECT new com.cruru.applicant.domain.dto.ApplicantCard( a.id, a.name, a.createdDate, a.isRejected, COUNT(e), COALESCE(AVG(e.score), 0.00), a.process.id ) FROM Applicant a - LEFT JOIN Evaluation e ON e.applicant = a + LEFT JOIN Evaluation e ON e.applicant = a AND e.process = a.process WHERE a.process IN :processes GROUP BY a.id, a.name, a.createdDate, a.isRejected, a.process.id """) @@ -39,7 +39,7 @@ a.id, a.name, a.createdDate, a.isRejected, COUNT(e), COALESCE(AVG(e.score), 0.00 a.id, a.name, a.createdDate, a.isRejected, COUNT(e), COALESCE(AVG(e.score), 0.00), a.process.id ) FROM Applicant a - LEFT JOIN Evaluation e ON e.applicant = a + LEFT JOIN Evaluation e ON e.applicant = a AND e.process = a.process WHERE a.process = :process GROUP BY a.id, a.name, a.createdDate, a.isRejected """) diff --git a/backend/src/main/java/com/cruru/applicant/service/ApplicantService.java b/backend/src/main/java/com/cruru/applicant/service/ApplicantService.java index 7a01e36df..7df01162b 100644 --- a/backend/src/main/java/com/cruru/applicant/service/ApplicantService.java +++ b/backend/src/main/java/com/cruru/applicant/service/ApplicantService.java @@ -30,10 +30,6 @@ public Applicant create(ApplicantCreateRequest request, Process firstProcess) { return applicantRepository.save(new Applicant(request.name(), request.email(), request.phone(), firstProcess)); } - public List findAllByProcess(Process process) { - return applicantRepository.findAllByProcess(process); - } - @Transactional public void updateApplicantInformation(long applicantId, ApplicantUpdateRequest request) { Applicant applicant = findById(applicantId); @@ -147,8 +143,16 @@ public List findAllByProcesses(List processes) { .toList(); } + public List findAllByProcess(Process process) { + return applicantRepository.findAllByProcess(process); + } + @Transactional public void deleteAllInBatch(List applicants) { applicantRepository.deleteAllInBatch(applicants); } + + public List findAllByIds(List ids) { + return applicantRepository.findAllById(ids); + } } diff --git a/backend/src/main/java/com/cruru/applyform/controller/ApplyFormController.java b/backend/src/main/java/com/cruru/applyform/controller/ApplyFormController.java index 4731fa767..afa47b194 100644 --- a/backend/src/main/java/com/cruru/applyform/controller/ApplyFormController.java +++ b/backend/src/main/java/com/cruru/applyform/controller/ApplyFormController.java @@ -30,14 +30,14 @@ public class ApplyFormController { @PostMapping("/{applyformId}/submit") public ResponseEntity submit( @RequestBody @Valid ApplyFormSubmitRequest request, - @PathVariable("applyformId") long applyFormId + @PathVariable("applyformId") Long applyFormId ) { applyFormFacade.submit(applyFormId, request); return ResponseEntity.created(URI.create("/v1/applyform/" + applyFormId)).build(); } @GetMapping("/{applyformId}") - public ResponseEntity read(@PathVariable("applyformId") long applyFormId) { + public ResponseEntity read(@PathVariable("applyformId") Long applyFormId) { ApplyFormResponse response = applyFormFacade.readApplyFormById(applyFormId); return ResponseEntity.ok(response); } diff --git a/backend/src/main/java/com/cruru/applyform/domain/ApplyForm.java b/backend/src/main/java/com/cruru/applyform/domain/ApplyForm.java index 8cfba61e6..5dfe1262b 100644 --- a/backend/src/main/java/com/cruru/applyform/domain/ApplyForm.java +++ b/backend/src/main/java/com/cruru/applyform/domain/ApplyForm.java @@ -4,11 +4,11 @@ import com.cruru.applyform.exception.badrequest.StartDateAfterEndDateException; import com.cruru.auth.util.SecureResource; import com.cruru.dashboard.domain.Dashboard; +import com.cruru.global.util.TsidSupplier; import com.cruru.member.domain.Member; +import io.hypersistence.utils.hibernate.id.Tsid; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; @@ -27,7 +27,7 @@ public class ApplyForm extends BaseEntity implements SecureResource { @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) + @Tsid(TsidSupplier.class) @Column(name = "apply_form_id") private Long id; diff --git a/backend/src/main/java/com/cruru/auth/domain/RefreshToken.java b/backend/src/main/java/com/cruru/auth/domain/RefreshToken.java index d8aa5ebeb..933241068 100644 --- a/backend/src/main/java/com/cruru/auth/domain/RefreshToken.java +++ b/backend/src/main/java/com/cruru/auth/domain/RefreshToken.java @@ -1,45 +1,16 @@ package com.cruru.auth.domain; -import com.cruru.BaseEntity; -import com.cruru.member.domain.Member; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToOne; import java.util.Objects; -import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; -import lombok.NoArgsConstructor; -@Entity -@NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Getter -public class RefreshToken extends BaseEntity implements Token { +public class RefreshToken implements Token { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "refresh_token_id") - private Long id; - - @Column(nullable = false, unique = true) private String token; - @OneToOne - @JoinColumn(name = "member_id", nullable = false) - private Member member; - - public RefreshToken(String token, Member member) { - this(null, token, member); - } - - public boolean isSameToken(String token) { - return this.token.equals(token); - } + private String email; @Override public boolean equals(Object o) { @@ -50,20 +21,19 @@ public boolean equals(Object o) { return false; } RefreshToken that = (RefreshToken) o; - return Objects.equals(getId(), that.getId()); + return Objects.equals(getToken(), that.getToken()); } @Override public int hashCode() { - return Objects.hashCode(getId()); + return Objects.hashCode(getToken()); } @Override public String toString() { return "RefreshToken{" + - "id=" + id + - ", token='" + token + '\'' + - ", member=" + member + + "token='" + token + '\'' + + ", email='" + email + '\'' + '}'; } } diff --git a/backend/src/main/java/com/cruru/auth/domain/repository/RefreshTokenRepository.java b/backend/src/main/java/com/cruru/auth/domain/repository/RefreshTokenRepository.java deleted file mode 100644 index d59099465..000000000 --- a/backend/src/main/java/com/cruru/auth/domain/repository/RefreshTokenRepository.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.cruru.auth.domain.repository; - -import com.cruru.auth.domain.RefreshToken; -import com.cruru.member.domain.Member; -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -public interface RefreshTokenRepository extends JpaRepository { - - boolean existsByToken(String token); - - boolean existsByMember(Member member); - - Optional findByMember(Member member); - - @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query("UPDATE RefreshToken rt SET rt.token = :refreshToken WHERE rt.member = :member") - void updateRefreshTokenByMember(@Param("refreshToken") String refreshToken, @Param("member") Member member); -} diff --git a/backend/src/main/java/com/cruru/auth/facade/AuthFacade.java b/backend/src/main/java/com/cruru/auth/facade/AuthFacade.java index 1c9c1cab6..aaa1ef4f3 100644 --- a/backend/src/main/java/com/cruru/auth/facade/AuthFacade.java +++ b/backend/src/main/java/com/cruru/auth/facade/AuthFacade.java @@ -30,8 +30,8 @@ public TokenResponse login(LoginRequest request) { } private TokenResponse createTokens(Member member) { - Token accessToken = authService.createAccessToken(member); - Token refreshToken = authService.createRefreshToken(member); + Token accessToken = authService.createAccessToken(member.getEmail(), member.getRole()); + Token refreshToken = authService.createRefreshToken(member.getEmail(), member.getRole()); return new TokenResponse(accessToken.getToken(), refreshToken.getToken()); } } diff --git a/backend/src/main/java/com/cruru/auth/service/AuthService.java b/backend/src/main/java/com/cruru/auth/service/AuthService.java index f0b617a90..d487d8a54 100644 --- a/backend/src/main/java/com/cruru/auth/service/AuthService.java +++ b/backend/src/main/java/com/cruru/auth/service/AuthService.java @@ -4,14 +4,12 @@ import com.cruru.auth.domain.AccessToken; import com.cruru.auth.domain.RefreshToken; import com.cruru.auth.domain.Token; -import com.cruru.auth.domain.repository.RefreshTokenRepository; import com.cruru.auth.exception.IllegalTokenException; import com.cruru.auth.exception.LoginExpiredException; import com.cruru.auth.security.PasswordValidator; import com.cruru.auth.security.TokenProperties; import com.cruru.auth.security.TokenProvider; -import com.cruru.member.domain.Member; -import com.cruru.member.domain.repository.MemberRepository; +import com.cruru.member.domain.MemberRole; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import java.util.HashMap; @@ -31,44 +29,42 @@ public class AuthService { private final TokenProvider tokenProvider; private final PasswordValidator passwordValidator; private final TokenProperties tokenProperties; - private final RefreshTokenRepository refreshTokenRepository; - private final MemberRepository memberRepository; + private final TokenRedisClient tokenRedisClient; - public Token createAccessToken(Member member) { - Map claims = getClaims(member); + public Token createAccessToken(String email, MemberRole role) { + Map claims = getClaims(email, role); String token = tokenProvider.createToken(claims, tokenProperties.accessExpireLength()); return new AccessToken(token); } @Transactional - public Token createRefreshToken(Member member) { - if (refreshTokenRepository.existsByMember(member)) { - return rotateRefreshToken(member); + public Token createRefreshToken(String email, MemberRole role) { + if (tokenRedisClient.existsByEmail(email)) { + return rotateRefreshToken(email, role); } - Map claims = getClaims(member); + Map claims = getClaims(email, role); String token = tokenProvider.createToken(claims, tokenProperties.refreshExpireLength()); + tokenRedisClient.saveToken(email, token); - return refreshTokenRepository.save(new RefreshToken(token, member)); + return new RefreshToken(token, email); } @Transactional public TokenResponse refresh(String refreshToken) { - checkRefreshTokenExists(refreshToken); - String email = extractEmail(refreshToken); - Member member = memberRepository.findByEmail(email) - .orElseThrow(); - validMemberRefreshToken(refreshToken, member); - return rotateTokens(member); + checkRefreshTokenExists(email, refreshToken); + MemberRole role = MemberRole.valueOf(extractMemberRole(refreshToken)); + validMemberRefreshToken(refreshToken, email); + return rotateTokens(email, role); } - private void checkRefreshTokenExists(String refreshToken) { + private void checkRefreshTokenExists(String email, String refreshToken) { if (!isTokenSignatureValid(refreshToken)) { throw new IllegalTokenException(); } - if (!refreshTokenRepository.existsByToken(refreshToken)) { + if (!tokenRedisClient.existsByToken(email, refreshToken)) { throw new IllegalTokenException(); } @@ -77,26 +73,26 @@ private void checkRefreshTokenExists(String refreshToken) { } } - private TokenResponse rotateTokens(Member member) { - Token accessToken = createAccessToken(member); - Token refreshToken = rotateRefreshToken(member); + private TokenResponse rotateTokens(String email, MemberRole role) { + Token accessToken = createAccessToken(email, role); + Token refreshToken = rotateRefreshToken(email, role); return new TokenResponse(accessToken.getToken(), refreshToken.getToken()); } - private Token rotateRefreshToken(Member member) { - Map claims = getClaims(member); + private Token rotateRefreshToken(String email, MemberRole role) { + Map claims = getClaims(email, role); String token = tokenProvider.createToken(claims, tokenProperties.refreshExpireLength()); - RefreshToken refreshToken = new RefreshToken(token, member); + RefreshToken refreshToken = new RefreshToken(token, email); - refreshTokenRepository.updateRefreshTokenByMember(refreshToken.getToken(), member); + tokenRedisClient.saveToken(email, refreshToken.getToken()); return refreshToken; } - private Map getClaims(Member member) { - Map claims = new HashMap<>(); - claims.put(EMAIL_CLAIM, member.getEmail()); - claims.put(ROLE_CLAIM, member.getRole().name()); - return claims; + private Map getClaims(String email, MemberRole role) { + return Map.of( + EMAIL_CLAIM, email, + ROLE_CLAIM, role.name() + ); } public boolean isTokenExpired(String token) { @@ -137,10 +133,10 @@ public boolean isNotVerifiedPassword(String rawPassword, String encodedPassword) return !passwordValidator.matches(rawPassword, encodedPassword); } - private void validMemberRefreshToken(String refreshToken, Member member) { - RefreshToken foundToken = refreshTokenRepository.findByMember(member) + private void validMemberRefreshToken(String refreshToken, String email) { + String foundToken = tokenRedisClient.getToken(email) .orElseThrow(IllegalTokenException::new); - if (!foundToken.isSameToken(refreshToken)) { + if (!foundToken.equals(refreshToken)) { throw new IllegalTokenException(); } } diff --git a/backend/src/main/java/com/cruru/auth/service/TokenRedisClient.java b/backend/src/main/java/com/cruru/auth/service/TokenRedisClient.java new file mode 100644 index 000000000..48dfcea5a --- /dev/null +++ b/backend/src/main/java/com/cruru/auth/service/TokenRedisClient.java @@ -0,0 +1,41 @@ +package com.cruru.auth.service; + +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class TokenRedisClient { + + private static final String TOKEN_PREFIX = "token:"; + private static final int TOKEN_EXPIRATION = 10; + + private final RedisTemplate redisTemplate; + + public void saveToken(String email, String token) { + redisTemplate.opsForValue() + .set( + TOKEN_PREFIX + email, + token, + TOKEN_EXPIRATION, + TimeUnit.MINUTES + ); + } + + public Optional getToken(String email) { + return Optional.ofNullable(redisTemplate.opsForValue().get(TOKEN_PREFIX + email)); + } + + public boolean existsByEmail(String email) { + return Boolean.TRUE.equals(redisTemplate.hasKey(TOKEN_PREFIX + email)); + } + + public boolean existsByToken(String email, String token) { + return getToken(email) + .map(storedToken -> storedToken.equals(token)) + .orElse(false); + } +} diff --git a/backend/src/main/java/com/cruru/config/DataSourceConfig.java b/backend/src/main/java/com/cruru/config/DataSourceConfig.java index 6f52300e7..847e1d753 100644 --- a/backend/src/main/java/com/cruru/config/DataSourceConfig.java +++ b/backend/src/main/java/com/cruru/config/DataSourceConfig.java @@ -19,20 +19,11 @@ public class DataSourceConfig { private static final String WRITE_DATASOURCE = "writeDataSource"; private static final String ROUTE_DATASOURCE = "routeDataSource"; - @Bean(name = READ_DATASOURCE) - @ConfigurationProperties(prefix = "spring.datasource.read") - public DataSource readDataSource() { - return DataSourceBuilder.create() - .type(HikariDataSource.class) - .build(); - } - - @Bean(name = WRITE_DATASOURCE) - @ConfigurationProperties(prefix = "spring.datasource.write") - public DataSource writeDataSource() { - return DataSourceBuilder.create() - .type(HikariDataSource.class) - .build(); + @Bean + @Primary + @DependsOn(ROUTE_DATASOURCE) + public DataSource defaultDataSource() { + return new LazyConnectionDataSourceProxy(routeDataSource()); } @Bean(name = ROUTE_DATASOURCE) @@ -50,10 +41,19 @@ public DataSourceRouter routeDataSource() { return dataSourceRouter; } - @Bean - @Primary - @DependsOn(ROUTE_DATASOURCE) - public DataSource defaultDataSource() { - return new LazyConnectionDataSourceProxy(routeDataSource()); + @Bean(name = WRITE_DATASOURCE) + @ConfigurationProperties(prefix = "spring.datasource.write") + public DataSource writeDataSource() { + return DataSourceBuilder.create() + .type(HikariDataSource.class) + .build(); + } + + @Bean(name = READ_DATASOURCE) + @ConfigurationProperties(prefix = "spring.datasource.read") + public DataSource readDataSource() { + return DataSourceBuilder.create() + .type(HikariDataSource.class) + .build(); } } diff --git a/backend/src/main/java/com/cruru/config/WebMvcConfig.java b/backend/src/main/java/com/cruru/config/WebMvcConfig.java index c896bf63e..0d331cd04 100644 --- a/backend/src/main/java/com/cruru/config/WebMvcConfig.java +++ b/backend/src/main/java/com/cruru/config/WebMvcConfig.java @@ -42,6 +42,8 @@ public void addInterceptors(InterceptorRegistry registry) { .excludePathPatterns("/**/signup") .excludePathPatterns("/**/login") .excludePathPatterns("/**/applyform/*/submit") + .excludePathPatterns("/**/emails/verification-code") + .excludePathPatterns("/**/emails/verify-code") .excludePathPatterns("/"); } diff --git a/backend/src/main/java/com/cruru/dashboard/controller/response/DashboardCreateResponse.java b/backend/src/main/java/com/cruru/dashboard/controller/response/DashboardCreateResponse.java index 4023ca526..21d5065e8 100644 --- a/backend/src/main/java/com/cruru/dashboard/controller/response/DashboardCreateResponse.java +++ b/backend/src/main/java/com/cruru/dashboard/controller/response/DashboardCreateResponse.java @@ -1,8 +1,11 @@ package com.cruru.dashboard.controller.response; public record DashboardCreateResponse( - long applyFormId, + String applyFormId, long dashboardId ) { + public static DashboardCreateResponse of(long applyFormId, long dashboardId) { + return new DashboardCreateResponse(String.valueOf(applyFormId), dashboardId); + } } diff --git a/backend/src/main/java/com/cruru/dashboard/controller/response/DashboardPreviewResponse.java b/backend/src/main/java/com/cruru/dashboard/controller/response/DashboardPreviewResponse.java index 43e9d11bd..578c93b7a 100644 --- a/backend/src/main/java/com/cruru/dashboard/controller/response/DashboardPreviewResponse.java +++ b/backend/src/main/java/com/cruru/dashboard/controller/response/DashboardPreviewResponse.java @@ -8,7 +8,7 @@ public record DashboardPreviewResponse( long dashboardId, - long applyFormId, + String applyFormId, String title, @@ -21,4 +21,14 @@ public record DashboardPreviewResponse( LocalDateTime endDate ) { + public static DashboardPreviewResponse of( + long dashboardId, + long applyFormId, + String title, + StatsResponse stats, + LocalDateTime startDate, + LocalDateTime endDate + ) { + return new DashboardPreviewResponse(dashboardId, String.valueOf(applyFormId), title, stats, startDate, endDate); + } } diff --git a/backend/src/main/java/com/cruru/dashboard/facade/DashboardFacade.java b/backend/src/main/java/com/cruru/dashboard/facade/DashboardFacade.java index 3ac1c9681..9e1e83da6 100644 --- a/backend/src/main/java/com/cruru/dashboard/facade/DashboardFacade.java +++ b/backend/src/main/java/com/cruru/dashboard/facade/DashboardFacade.java @@ -57,7 +57,7 @@ public DashboardCreateResponse create(long clubId, DashboardCreateRequest reques for (QuestionCreateRequest questionCreateRequest : request.questions()) { questionService.create(questionCreateRequest, applyForm); } - return new DashboardCreateResponse(applyForm.getId(), dashboard.getId()); + return DashboardCreateResponse.of(applyForm.getId(), dashboard.getId()); } private ApplyFormWriteRequest toApplyFormWriteRequest(DashboardCreateRequest request) { @@ -89,7 +89,7 @@ private DashboardPreviewResponse createDashboardPreviewResponse(DashboardApplyFo List applicants = dashboardService.findAllApplicants(dashboard); StatsResponse stats = calculateStats(applicants); - return new DashboardPreviewResponse( + return DashboardPreviewResponse.of( dashboard.getId(), applyForm.getId(), applyForm.getTitle(), diff --git a/backend/src/main/java/com/cruru/email/controller/EmailController.java b/backend/src/main/java/com/cruru/email/controller/EmailController.java index 8f624b8f8..f478d24e1 100644 --- a/backend/src/main/java/com/cruru/email/controller/EmailController.java +++ b/backend/src/main/java/com/cruru/email/controller/EmailController.java @@ -1,7 +1,9 @@ package com.cruru.email.controller; import com.cruru.auth.annotation.ValidAuth; -import com.cruru.email.controller.dto.EmailRequest; +import com.cruru.email.controller.request.EmailRequest; +import com.cruru.email.controller.request.SendVerificationCodeRequest; +import com.cruru.email.controller.request.VerifyCodeRequest; import com.cruru.email.facade.EmailFacade; import com.cruru.global.LoginProfile; import jakarta.validation.Valid; @@ -9,6 +11,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -28,4 +31,18 @@ public ResponseEntity send( emailFacade.send(request); return ResponseEntity.ok().build(); } + + @PostMapping("/verification-code") + public ResponseEntity sendVerificationCode( + @RequestBody @Valid SendVerificationCodeRequest request + ) { + emailFacade.sendVerificationCode(request); + return ResponseEntity.ok().build(); + } + + @PostMapping("/verify-code") + public ResponseEntity verifyCode(@Valid @RequestBody VerifyCodeRequest request) { + emailFacade.verifyCode(request); + return ResponseEntity.ok().build(); + } } diff --git a/backend/src/main/java/com/cruru/email/controller/dto/EmailRequest.java b/backend/src/main/java/com/cruru/email/controller/request/EmailRequest.java similarity index 95% rename from backend/src/main/java/com/cruru/email/controller/dto/EmailRequest.java rename to backend/src/main/java/com/cruru/email/controller/request/EmailRequest.java index 946b29d09..c3356907d 100644 --- a/backend/src/main/java/com/cruru/email/controller/dto/EmailRequest.java +++ b/backend/src/main/java/com/cruru/email/controller/request/EmailRequest.java @@ -1,4 +1,4 @@ -package com.cruru.email.controller.dto; +package com.cruru.email.controller.request; import com.cruru.applicant.domain.Applicant; import com.cruru.auth.annotation.RequireAuth; diff --git a/backend/src/main/java/com/cruru/email/controller/request/SendVerificationCodeRequest.java b/backend/src/main/java/com/cruru/email/controller/request/SendVerificationCodeRequest.java new file mode 100644 index 000000000..1d002e8ba --- /dev/null +++ b/backend/src/main/java/com/cruru/email/controller/request/SendVerificationCodeRequest.java @@ -0,0 +1,12 @@ +package com.cruru.email.controller.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record SendVerificationCodeRequest( + @NotBlank + @Email + String email +) { + +} diff --git a/backend/src/main/java/com/cruru/email/controller/request/VerifyCodeRequest.java b/backend/src/main/java/com/cruru/email/controller/request/VerifyCodeRequest.java new file mode 100644 index 000000000..9caaae36a --- /dev/null +++ b/backend/src/main/java/com/cruru/email/controller/request/VerifyCodeRequest.java @@ -0,0 +1,15 @@ +package com.cruru.email.controller.request; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record VerifyCodeRequest( + @NotBlank + @Email + String email, + + @NotBlank + String verificationCode +) { + +} diff --git a/backend/src/main/java/com/cruru/email/exception/EmailConflictException.java b/backend/src/main/java/com/cruru/email/exception/EmailConflictException.java new file mode 100644 index 000000000..a26e3e8ba --- /dev/null +++ b/backend/src/main/java/com/cruru/email/exception/EmailConflictException.java @@ -0,0 +1,12 @@ +package com.cruru.email.exception; + +import com.cruru.advice.ConflictException; + +public class EmailConflictException extends ConflictException { + + private static final String MESSAGE = "이미 가입된 이메일입니다."; + + public EmailConflictException() { + super(MESSAGE); + } +} diff --git a/backend/src/main/java/com/cruru/email/exception/NotVerifiedEmailException.java b/backend/src/main/java/com/cruru/email/exception/NotVerifiedEmailException.java new file mode 100644 index 000000000..bad5e1b96 --- /dev/null +++ b/backend/src/main/java/com/cruru/email/exception/NotVerifiedEmailException.java @@ -0,0 +1,13 @@ +package com.cruru.email.exception; + +import com.cruru.advice.UnauthorizedException; + +public class NotVerifiedEmailException extends UnauthorizedException { + + private static final String MESSAGE = "이메일 인증이 필요합니다."; + + public NotVerifiedEmailException() { + super(MESSAGE); + } +} + diff --git a/backend/src/main/java/com/cruru/email/exception/badrequest/VerificationCodeMismatchException.java b/backend/src/main/java/com/cruru/email/exception/badrequest/VerificationCodeMismatchException.java new file mode 100644 index 000000000..5cf6f9bfd --- /dev/null +++ b/backend/src/main/java/com/cruru/email/exception/badrequest/VerificationCodeMismatchException.java @@ -0,0 +1,12 @@ +package com.cruru.email.exception.badrequest; + +import com.cruru.advice.badrequest.BadRequestException; + +public class VerificationCodeMismatchException extends BadRequestException { + + private static final String MESSAGE = "인증 코드가 일치하지 않습니다."; + + public VerificationCodeMismatchException() { + super(MESSAGE); + } +} diff --git a/backend/src/main/java/com/cruru/email/exception/badrequest/VerificationCodeNotFoundException.java b/backend/src/main/java/com/cruru/email/exception/badrequest/VerificationCodeNotFoundException.java new file mode 100644 index 000000000..c37b14395 --- /dev/null +++ b/backend/src/main/java/com/cruru/email/exception/badrequest/VerificationCodeNotFoundException.java @@ -0,0 +1,12 @@ +package com.cruru.email.exception.badrequest; + +import com.cruru.advice.badrequest.BadRequestException; + +public class VerificationCodeNotFoundException extends BadRequestException { + + private static final String MESSAGE = "인증 코드가 존재하지 않거나 만료되었습니다."; + + public VerificationCodeNotFoundException() { + super(MESSAGE); + } +} diff --git a/backend/src/main/java/com/cruru/email/facade/EmailFacade.java b/backend/src/main/java/com/cruru/email/facade/EmailFacade.java index b68ee189a..b3a743c56 100644 --- a/backend/src/main/java/com/cruru/email/facade/EmailFacade.java +++ b/backend/src/main/java/com/cruru/email/facade/EmailFacade.java @@ -4,10 +4,16 @@ import com.cruru.applicant.service.ApplicantService; import com.cruru.club.domain.Club; import com.cruru.club.service.ClubService; -import com.cruru.email.controller.dto.EmailRequest; +import com.cruru.email.controller.request.EmailRequest; +import com.cruru.email.controller.request.SendVerificationCodeRequest; +import com.cruru.email.controller.request.VerifyCodeRequest; import com.cruru.email.exception.EmailAttachmentsException; +import com.cruru.email.exception.EmailConflictException; +import com.cruru.email.service.EmailRedisClient; import com.cruru.email.service.EmailService; import com.cruru.email.util.FileUtil; +import com.cruru.email.util.VerificationCodeUtil; +import com.cruru.member.service.MemberService; import java.io.File; import java.io.IOException; import java.util.List; @@ -23,13 +29,12 @@ public class EmailFacade { private final EmailService emailService; private final ClubService clubService; private final ApplicantService applicantService; + private final MemberService memberService; + private final EmailRedisClient emailRedisClient; public void send(EmailRequest request) { Club from = clubService.findById(request.clubId()); - List applicants = request.applicantIds() - .stream() - .map(applicantService::findById) - .toList(); + List applicants = applicantService.findAllByIds(request.applicantIds()); sendAndSave(from, applicants, request.subject(), request.content(), request.files()); } @@ -52,4 +57,27 @@ private List saveTempFiles(Club from, String subject, List throw new EmailAttachmentsException(from.getId(), subject); } } + + public void sendVerificationCode(SendVerificationCodeRequest request) { + String email = request.email(); + String verificationCode = VerificationCodeUtil.generateVerificationCode(); + validateEmailExists(email); + emailRedisClient.saveVerificationCode(email, verificationCode); + emailService.sendVerificationCode(email, verificationCode); + } + + private void validateEmailExists(String email) { + if (memberService.existsByEmail(email)) { + throw new EmailConflictException(); + } + } + + public void verifyCode(VerifyCodeRequest request) { + String email = request.email(); + String inputVerificationCode = request.verificationCode(); + String storedVerificationCode = emailRedisClient.getVerificationCode(email); + + VerificationCodeUtil.verify(storedVerificationCode, inputVerificationCode); + emailRedisClient.saveVerifiedEmail(email); + } } diff --git a/backend/src/main/java/com/cruru/email/service/EmailRedisClient.java b/backend/src/main/java/com/cruru/email/service/EmailRedisClient.java new file mode 100644 index 000000000..b0eb2a7fd --- /dev/null +++ b/backend/src/main/java/com/cruru/email/service/EmailRedisClient.java @@ -0,0 +1,55 @@ +package com.cruru.email.service; + +import com.cruru.email.exception.NotVerifiedEmailException; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class EmailRedisClient { + + private static final String VERIFICATION_CODE_PREFIX = "email_verification_code:"; + private static final String VERIFIED_EMAIL_PREFIX = "email_verified:"; + private static final int VERIFICATION_CODE_EXPIRATION = 10; + private static final int VERIFIED_EMAIL_EXPIRATION = 10; + + private final RedisTemplate redisTemplate; + + public void saveVerificationCode(String email, String verificationCode) { + redisTemplate.opsForValue() + .set( + VERIFICATION_CODE_PREFIX + email, + verificationCode, + VERIFICATION_CODE_EXPIRATION, + TimeUnit.MINUTES + ); + } + + public String getVerificationCode(String email) { + return redisTemplate.opsForValue().get(VERIFICATION_CODE_PREFIX + email); + } + + public void saveVerifiedEmail(String email) { + redisTemplate.opsForValue() + .set( + VERIFIED_EMAIL_PREFIX + email, + VerificationStatus.VERIFIED.getValue(), + VERIFIED_EMAIL_EXPIRATION, + TimeUnit.MINUTES + ); + } + + public void verifyEmail(String email) { + String verifiedStatus = redisTemplate.opsForValue().get(VERIFIED_EMAIL_PREFIX + email); + VerificationStatus status = VerificationStatus.fromValue(verifiedStatus); + checkVerification(status); + } + + private void checkVerification(VerificationStatus status) { + if (!status.isVerified()) { + throw new NotVerifiedEmailException(); + } + } +} diff --git a/backend/src/main/java/com/cruru/email/service/EmailService.java b/backend/src/main/java/com/cruru/email/service/EmailService.java index 1321d2e14..386e8dec5 100644 --- a/backend/src/main/java/com/cruru/email/service/EmailService.java +++ b/backend/src/main/java/com/cruru/email/service/EmailService.java @@ -4,6 +4,7 @@ import com.cruru.club.domain.Club; import com.cruru.email.domain.Email; import com.cruru.email.domain.repository.EmailRepository; +import com.cruru.email.util.EmailTemplate; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import java.io.File; @@ -32,10 +33,10 @@ public CompletableFuture send( Club from, Applicant to, String subject, String content, List tempFiles) { try { MimeMessage message = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message, true); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); helper.setTo(to.getEmail()); - helper.setSubject(subject); - helper.setText(content); + helper.setSubject(EmailTemplate.defaultEmailSubject(from.getName(), subject)); + helper.setText(EmailTemplate.generateDefaultEmailTemplate(from.getName(), subject, content), true); if (hasFile(tempFiles)) { addAttachments(helper, tempFiles); } @@ -79,4 +80,21 @@ public void save(Email email) { public void deleteAllByTos(List applicants) { emailRepository.deleteAllByTos(applicants); } + + @Async + public void sendVerificationCode(String to, String verificationCode) { + try { + String subject = "[크루루] 인증 코드 안내"; + String content = EmailTemplate.generateVerificationEmailContent(verificationCode); + + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, false, "UTF-8"); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(content, true); + mailSender.send(message); + } catch (MessagingException | MailException e) { + log.error("이메일 전송 실패: to={}, subject={}", to, e.getMessage()); + } + } } diff --git a/backend/src/main/java/com/cruru/email/service/VerificationStatus.java b/backend/src/main/java/com/cruru/email/service/VerificationStatus.java new file mode 100644 index 000000000..73fcec8f1 --- /dev/null +++ b/backend/src/main/java/com/cruru/email/service/VerificationStatus.java @@ -0,0 +1,29 @@ +package com.cruru.email.service; + +import java.util.Arrays; +import lombok.Getter; + +@Getter +public enum VerificationStatus { + + VERIFIED("verified"), + NOT_VERIFIED("not_verified"), + ; + + private final String value; + + VerificationStatus(String value) { + this.value = value; + } + + public static VerificationStatus fromValue(String value) { + return Arrays.stream(VerificationStatus.values()) + .filter(status -> status.getValue().equalsIgnoreCase(value)) + .findFirst() + .orElse(NOT_VERIFIED); + } + + public boolean isVerified() { + return this == VERIFIED; + } +} diff --git a/backend/src/main/java/com/cruru/email/util/EmailTemplate.java b/backend/src/main/java/com/cruru/email/util/EmailTemplate.java new file mode 100644 index 000000000..12a4db581 --- /dev/null +++ b/backend/src/main/java/com/cruru/email/util/EmailTemplate.java @@ -0,0 +1,52 @@ +package com.cruru.email.util; + +public class EmailTemplate { + + public static String generateVerificationEmailContent(String verificationCode) { + return """ +
+
+

크루루

+
+
+

[크루루] 인증 코드 안내

+

안녕하세요,

+

아래 인증 코드를 입력해 주세요:

+
+ %s +
+

이 코드는 10분 후에 만료됩니다.

+
+
+
+

본 메일은 크루루 시스템에 의해 자동 발송되었습니다.

+

© 2024 크루루. All Rights Reserved.

+
+
+ """.formatted(verificationCode); + } + + public static String generateDefaultEmailTemplate(String sender, String title, String content) { + return """ +
+
+

%s

+
+
+

%s

+

%s

+
+
+
+

본 메일은 수신 전용으로 답장이 불가하며, 크루루(Cruru)를 통해 발송되었습니다.

+

복잡했던 리크루팅 하루만에 크루루!

+

© 2024 크루루. All Rights Reserved.

+
+
+ """.formatted(sender, title, content.replace(System.lineSeparator(), "
")); + } + + public static String defaultEmailSubject(String sender, String title) { + return "[%s] %s".formatted(sender, title); + } +} diff --git a/backend/src/main/java/com/cruru/email/util/VerificationCodeUtil.java b/backend/src/main/java/com/cruru/email/util/VerificationCodeUtil.java new file mode 100644 index 000000000..4d166b822 --- /dev/null +++ b/backend/src/main/java/com/cruru/email/util/VerificationCodeUtil.java @@ -0,0 +1,28 @@ +package com.cruru.email.util; + +import com.cruru.email.exception.badrequest.VerificationCodeMismatchException; +import com.cruru.email.exception.badrequest.VerificationCodeNotFoundException; +import java.util.Random; + +public class VerificationCodeUtil { + + private static final Random random = new Random(); + private static final int CODE_LENGTH = 6; + + private VerificationCodeUtil() { + } + + public static String generateVerificationCode() { + return String.format("%0" + CODE_LENGTH + "d", random.nextInt((int) Math.pow(10, CODE_LENGTH))); + } + + public static void verify(String storedVerificationCode, String inputVerificationCode) { + if (storedVerificationCode == null) { + throw new VerificationCodeNotFoundException(); + } + + if (!storedVerificationCode.equals(inputVerificationCode)) { + throw new VerificationCodeMismatchException(); + } + } +} diff --git a/backend/src/main/java/com/cruru/global/util/TsidSupplier.java b/backend/src/main/java/com/cruru/global/util/TsidSupplier.java new file mode 100644 index 000000000..5b0779373 --- /dev/null +++ b/backend/src/main/java/com/cruru/global/util/TsidSupplier.java @@ -0,0 +1,14 @@ +package com.cruru.global.util; + +import io.hypersistence.tsid.TSID.Factory; +import java.util.function.Supplier; + +public class TsidSupplier implements Supplier { + + @Override + public Factory get() { + return Factory.builder() + .withNodeBits(10) + .build(); + } +} diff --git a/backend/src/main/java/com/cruru/member/facade/MemberFacade.java b/backend/src/main/java/com/cruru/member/facade/MemberFacade.java index e0f02a956..248fa416a 100644 --- a/backend/src/main/java/com/cruru/member/facade/MemberFacade.java +++ b/backend/src/main/java/com/cruru/member/facade/MemberFacade.java @@ -1,6 +1,7 @@ package com.cruru.member.facade; import com.cruru.club.service.ClubService; +import com.cruru.email.service.EmailRedisClient; import com.cruru.member.controller.request.MemberCreateRequest; import com.cruru.member.domain.Member; import com.cruru.member.service.MemberService; @@ -15,9 +16,11 @@ public class MemberFacade { private final MemberService memberService; private final ClubService clubService; + private final EmailRedisClient emailRedisClient; @Transactional public long create(MemberCreateRequest request) { + emailRedisClient.verifyEmail(request.email()); Member savedMember = memberService.create(request); clubService.create(request.clubName(), savedMember); return savedMember.getId(); diff --git a/backend/src/main/java/com/cruru/member/service/MemberService.java b/backend/src/main/java/com/cruru/member/service/MemberService.java index 09785871d..14478973f 100644 --- a/backend/src/main/java/com/cruru/member/service/MemberService.java +++ b/backend/src/main/java/com/cruru/member/service/MemberService.java @@ -62,4 +62,8 @@ public Member findByEmail(String email) { return memberRepository.findByEmail(email) .orElseThrow(MemberNotFoundException::new); } + + public boolean existsByEmail(String email) { + return memberRepository.existsByEmail(email); + } } diff --git a/backend/src/main/java/com/cruru/process/controller/response/ProcessResponses.java b/backend/src/main/java/com/cruru/process/controller/response/ProcessResponses.java index 5de82f4f0..4cd84af1c 100644 --- a/backend/src/main/java/com/cruru/process/controller/response/ProcessResponses.java +++ b/backend/src/main/java/com/cruru/process/controller/response/ProcessResponses.java @@ -6,7 +6,7 @@ public record ProcessResponses( - long applyFormId, + String applyFormId, @JsonProperty("processes") List processResponses, @@ -18,4 +18,13 @@ public record ProcessResponses( LocalDateTime endDate ) { + public static ProcessResponses of( + long applyFormId, + List processResponses, + String title, + LocalDateTime startDate, + LocalDateTime endDate + ) { + return new ProcessResponses(String.valueOf(applyFormId), processResponses, title, startDate, endDate); + } } diff --git a/backend/src/main/java/com/cruru/process/facade/ProcessFacade.java b/backend/src/main/java/com/cruru/process/facade/ProcessFacade.java index 699f1a0ef..ca97e0b23 100644 --- a/backend/src/main/java/com/cruru/process/facade/ProcessFacade.java +++ b/backend/src/main/java/com/cruru/process/facade/ProcessFacade.java @@ -54,7 +54,7 @@ public ProcessResponses readAllByDashboardId( .map(process -> toProcessResponse(process, applicantCards)) .toList(); - return new ProcessResponses( + return ProcessResponses.of( applyForm.getId(), processResponses, applyForm.getTitle(), diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 20919a2dc..f41091e0c 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -47,9 +47,9 @@ spring: max-request-size: 50MB data: redis: - port: ${REDIS_PORT} - host: ${REDIS_HOST} - password: ${REDIS_PASSWORD} + port: 6379 + host: localhost + password: password security: jwt: diff --git a/backend/src/main/resources/db/migration/V2_1_5__delete_token_table.sql b/backend/src/main/resources/db/migration/V2_1_5__delete_token_table.sql new file mode 100644 index 000000000..5bb78451e --- /dev/null +++ b/backend/src/main/resources/db/migration/V2_1_5__delete_token_table.sql @@ -0,0 +1,8 @@ +ALTER TABLE refresh_token + DROP CONSTRAINT fk_refresh_token_to_member; +ALTER TABLE refresh_token + DROP CONSTRAINT uk_refresh_token_member_id; +ALTER TABLE refresh_token + DROP CONSTRAINT uk_refresh_token_token; + +DROP TABLE IF EXISTS refresh_token; diff --git a/backend/src/test/java/com/cruru/applicant/domain/repository/ApplicantRepositoryTest.java b/backend/src/test/java/com/cruru/applicant/domain/repository/ApplicantRepositoryTest.java index 5106ddede..eaedf0362 100644 --- a/backend/src/test/java/com/cruru/applicant/domain/repository/ApplicantRepositoryTest.java +++ b/backend/src/test/java/com/cruru/applicant/domain/repository/ApplicantRepositoryTest.java @@ -78,31 +78,31 @@ void saveNoId() { @Test void findApplicantCardsByProcesses() { // given - Process process = processRepository.save(ProcessFixture.applyType()); + Process process1 = processRepository.save(ProcessFixture.applyType()); + Process process2 = processRepository.save(ProcessFixture.interview(null)); - Applicant applicant1 = ApplicantFixture.pendingDobby(process); - Applicant applicant2 = ApplicantFixture.pendingRush(process); + Applicant applicant1 = ApplicantFixture.pendingDobby(process1); + Applicant applicant2 = ApplicantFixture.pendingRush(process1); applicantRepository.saveAll(List.of(applicant1, applicant2)); List evaluations = List.of( - EvaluationFixture.fivePoints(process, applicant1), - EvaluationFixture.fivePoints(process, applicant1), - EvaluationFixture.fivePoints(process, applicant2) + EvaluationFixture.fourPoints(process1, applicant1), + EvaluationFixture.fivePoints(process2, applicant1), + EvaluationFixture.fivePoints(process1, applicant2) ); evaluationRepository.saveAll(evaluations); // when - List applicantCards = applicantRepository.findApplicantCardsByProcesses(List.of(process)); + List applicantCards = applicantRepository.findApplicantCardsByProcesses(List.of(process1)); // then assertThat(applicantCards).hasSize(2); - ApplicantCard applicantCard1 = applicantCards.get(0); assertAll( () -> assertThat(applicantCard1.id()).isEqualTo(applicant1.getId()), () -> assertThat(applicantCard1.name()).isEqualTo(applicant1.getName()), - () -> assertThat(applicantCard1.evaluationCount()).isEqualTo(2), - () -> assertThat(applicantCard1.averageScore()).isEqualTo(5.0) + () -> assertThat(applicantCard1.evaluationCount()).isEqualTo(1), + () -> assertThat(applicantCard1.averageScore()).isEqualTo(4.0) ); ApplicantCard applicantCard2 = applicantCards.get(1); @@ -140,39 +140,31 @@ void findApplicantCardsByProcesses_noEvaluations() { @Test void findApplicantCardsByProcess() { // given - Process process = processRepository.save(ProcessFixture.applyType()); + Process process1 = processRepository.save(ProcessFixture.applyType()); + Process process2 = processRepository.save(ProcessFixture.interview(null)); - Applicant applicant1 = ApplicantFixture.pendingDobby(process); - Applicant applicant2 = ApplicantFixture.pendingRush(process); + Applicant applicant1 = ApplicantFixture.pendingDobby(process1); + Applicant applicant2 = ApplicantFixture.pendingRush(process2); applicantRepository.saveAll(List.of(applicant1, applicant2)); List evaluations = List.of( - EvaluationFixture.fivePoints(process, applicant1), - EvaluationFixture.fivePoints(process, applicant1), - EvaluationFixture.fivePoints(process, applicant2) + EvaluationFixture.fourPoints(process1, applicant1), + EvaluationFixture.fivePoints(process2, applicant1), + EvaluationFixture.fivePoints(process1, applicant2) ); evaluationRepository.saveAll(evaluations); // when - List applicantCards = applicantRepository.findApplicantCardsByProcess(process); + List applicantCards = applicantRepository.findApplicantCardsByProcess(process1); // then - assertThat(applicantCards).hasSize(2); - - ApplicantCard applicantCard1 = applicantCards.get(0); - assertAll( - () -> assertThat(applicantCard1.id()).isEqualTo(applicant1.getId()), - () -> assertThat(applicantCard1.name()).isEqualTo(applicant1.getName()), - () -> assertThat(applicantCard1.evaluationCount()).isEqualTo(2), - () -> assertThat(applicantCard1.averageScore()).isEqualTo(5.0) - ); - - ApplicantCard applicantCard2 = applicantCards.get(1); + assertThat(applicantCards).hasSize(1); + ApplicantCard applicantCard = applicantCards.get(0); assertAll( - () -> assertThat(applicantCard2.id()).isEqualTo(applicant2.getId()), - () -> assertThat(applicantCard2.name()).isEqualTo(applicant2.getName()), - () -> assertThat(applicantCard2.evaluationCount()).isEqualTo(1), - () -> assertThat(applicantCard2.averageScore()).isEqualTo(5.0) + () -> assertThat(applicantCard.id()).isEqualTo(applicant1.getId()), + () -> assertThat(applicantCard.name()).isEqualTo(applicant1.getName()), + () -> assertThat(applicantCard.evaluationCount()).isEqualTo(1), + () -> assertThat(applicantCard.averageScore()).isEqualTo(4.0) ); } diff --git a/backend/src/test/java/com/cruru/applicant/service/ApplicantServiceTest.java b/backend/src/test/java/com/cruru/applicant/service/ApplicantServiceTest.java index 8a40a553a..db619299c 100644 --- a/backend/src/test/java/com/cruru/applicant/service/ApplicantServiceTest.java +++ b/backend/src/test/java/com/cruru/applicant/service/ApplicantServiceTest.java @@ -102,12 +102,12 @@ void findAllByProcess_filterAndOrder() { Applicant applicant2 = applicantRepository.save(ApplicantFixture.pendingRush(process1)); applicantRepository.save(ApplicantFixture.pendingDobby(process2)); - evaluationRepository.save(EvaluationFixture.fivePoints(process1, applicant1)); - evaluationRepository.save(EvaluationFixture.fourPoints(process1, applicant2)); + evaluationRepository.save(EvaluationFixture.fourPoints(process1, applicant1)); + evaluationRepository.save(EvaluationFixture.fivePoints(process1, applicant2)); String evaluationStatus = "EVALUATED"; - String sortByCreatedAt = "DESC"; - String sortByScore = null; + String sortByCreatedAt = null; + String sortByScore = "DESC"; // when List applicantCards = applicantService.findApplicantCards( @@ -258,4 +258,31 @@ void deleteAllInBatch() { assertThat(applicantRepository.findAll()).contains(applicant3) .doesNotContain(applicant1, applicant2); } + + @DisplayName("IN절을 활용하여 id로 지원자를 찾는다.") + @Test + void findAllByIds() { + // given + List applicants = applicantRepository.saveAll(List.of( + ApplicantFixture.pendingDobby(), ApplicantFixture.pendingDobby(), + ApplicantFixture.pendingDobby(), ApplicantFixture.pendingDobby(), + ApplicantFixture.pendingDobby(), ApplicantFixture.pendingDobby(), + ApplicantFixture.pendingDobby(), ApplicantFixture.pendingDobby(), + ApplicantFixture.pendingDobby(), ApplicantFixture.pendingDobby(), + ApplicantFixture.pendingDobby(), ApplicantFixture.pendingDobby(), + ApplicantFixture.pendingDobby(), ApplicantFixture.pendingDobby(), + ApplicantFixture.pendingDobby(), ApplicantFixture.pendingDobby(), + ApplicantFixture.pendingDobby(), ApplicantFixture.pendingDobby(), + ApplicantFixture.pendingDobby(), ApplicantFixture.pendingDobby() + )); + List ids = applicants.stream() + .map(Applicant::getId) + .toList(); + + // when + List found = applicantRepository.findAllById(ids); + + // then + assertThat(found).containsExactlyInAnyOrderElementsOf(applicants); + } } diff --git a/backend/src/test/java/com/cruru/applyform/controller/ApplyFormControllerTest.java b/backend/src/test/java/com/cruru/applyform/controller/ApplyFormControllerTest.java index 89790cb3c..8a5b63f21 100644 --- a/backend/src/test/java/com/cruru/applyform/controller/ApplyFormControllerTest.java +++ b/backend/src/test/java/com/cruru/applyform/controller/ApplyFormControllerTest.java @@ -110,7 +110,7 @@ private static Stream InvalidApplicantCreateRequest() { @DisplayName("지원서 폼 제출 시, 201을 반환한다.") @Test - void submit() { + void submitById() { // given Dashboard dashboard = dashboardRepository.save(DashboardFixture.backend()); processRepository.save(ProcessFixture.applyType(dashboard)); @@ -132,7 +132,7 @@ void submit() { RestAssured.given(spec).log().all() .contentType(ContentType.JSON) .body(request) - .filter(document("applyform/submit", + .filter(document("applyform/submit-id", pathParameters(parameterWithName("applyFormId").description("지원폼의 id")), requestFields(APPLICANT_SUBMIT_FIELD_DESCRIPTORS) .andWithPrefix("answers[].", ANSWER_SUBMIT_FIELD_DESCRIPTORS) @@ -380,7 +380,7 @@ void submit_RequiredNotReplied() { @DisplayName("지원서 폼 조회 시, 200을 반환한다.") @Test - void read() { + void readById() { // given Dashboard dashboard = dashboardRepository.save(DashboardFixture.backend()); processRepository.save(ProcessFixture.applyType(dashboard)); @@ -393,7 +393,7 @@ void read() { RestAssured.given(spec).log().all() .cookie("accessToken", token) .contentType(ContentType.JSON) - .filter(document("applicant/read-applyform", + .filter(document("applicant/read-applyform-id", pathParameters(parameterWithName("applyFormId").description("지원폼의 id")), responseFields( fieldWithPath("title").description("지원폼의 제목"), @@ -427,7 +427,7 @@ void read_notFound() { @DisplayName("지원서 폼을 성공적으로 수정하면, 200을 응답한다.") @Test - void update() { + void updateById() { // given String toChangeTitle = "크루루 백엔드 모집 공고~~"; String toChangeDescription = "# 모집 공고 설명 #"; @@ -444,7 +444,7 @@ void update() { .contentType(ContentType.JSON) .cookie("accessToken", token) .body(request) - .filter(document("applicant/update", + .filter(document("applicant/update-id", requestCookies(cookieWithName("accessToken").description("사용자 토큰")), pathParameters(parameterWithName("applyFormId").description("지원폼의 id")), requestFields(APPLYFORM_WRITE_FIELD_DESCRIPTORS) diff --git a/backend/src/test/java/com/cruru/applyform/domain/repository/ApplyFormRepositoryTest.java b/backend/src/test/java/com/cruru/applyform/domain/repository/ApplyFormRepositoryTest.java index fe28c6d23..180a52fc7 100644 --- a/backend/src/test/java/com/cruru/applyform/domain/repository/ApplyFormRepositoryTest.java +++ b/backend/src/test/java/com/cruru/applyform/domain/repository/ApplyFormRepositoryTest.java @@ -86,7 +86,7 @@ void save_NotSavedId() { ApplyForm savedApplyForm2 = applyFormRepository.save(applyForm2); //then - assertThat(savedApplyForm1.getId() + 1).isEqualTo(savedApplyForm2.getId()); + assertThat(savedApplyForm1.getId()).isLessThan(savedApplyForm2.getId()); } @DisplayName("특정 동아리에 속하는 DashboardApplyForm 목록을 반환한다.") diff --git a/backend/src/test/java/com/cruru/applyform/facade/ApplyFormFacadeTest.java b/backend/src/test/java/com/cruru/applyform/facade/ApplyFormFacadeTest.java index 1a2fa68cf..b9438f7fa 100644 --- a/backend/src/test/java/com/cruru/applyform/facade/ApplyFormFacadeTest.java +++ b/backend/src/test/java/com/cruru/applyform/facade/ApplyFormFacadeTest.java @@ -161,7 +161,7 @@ void submit_invalidSubmitDate() { @Test void submit_invalidApplyForm() { // given&when&then - assertThatThrownBy(() -> applyFormFacade.submit(-1, applyFormSubmitrequest)) + assertThatThrownBy(() -> applyFormFacade.submit(-1L, applyFormSubmitrequest)) .isInstanceOf(ApplyFormNotFoundException.class); } diff --git a/backend/src/test/java/com/cruru/auth/aspect/AuthValidationAspectTest.java b/backend/src/test/java/com/cruru/auth/aspect/AuthValidationAspectTest.java index ac17bea67..749177e2f 100644 --- a/backend/src/test/java/com/cruru/auth/aspect/AuthValidationAspectTest.java +++ b/backend/src/test/java/com/cruru/auth/aspect/AuthValidationAspectTest.java @@ -70,7 +70,8 @@ class AuthValidationAspectTest extends ControllerTest { @BeforeEach void setUp() { Member unauthorizedMember = memberRepository.save(MemberFixture.RUSH); - unauthorizedToken = authService.createAccessToken(unauthorizedMember).getToken(); + unauthorizedToken = authService.createAccessToken(unauthorizedMember.getEmail(), unauthorizedMember.getRole()) + .getToken(); clubRepository.save(ClubFixture.create(unauthorizedMember)); dashboard = dashboardRepository.save(DashboardFixture.backend(defaultClub)); applyForm = applyFormRepository.save(ApplyFormFixture.backend(dashboard)); diff --git a/backend/src/test/java/com/cruru/auth/controller/AuthControllerTest.java b/backend/src/test/java/com/cruru/auth/controller/AuthControllerTest.java index fddc5e998..fff1c2e45 100644 --- a/backend/src/test/java/com/cruru/auth/controller/AuthControllerTest.java +++ b/backend/src/test/java/com/cruru/auth/controller/AuthControllerTest.java @@ -1,5 +1,7 @@ package com.cruru.auth.controller; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; @@ -11,8 +13,7 @@ import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import com.cruru.auth.controller.request.LoginRequest; -import com.cruru.auth.domain.Token; -import com.cruru.auth.service.AuthService; +import com.cruru.auth.service.TokenRedisClient; import com.cruru.club.domain.repository.ClubRepository; import com.cruru.member.domain.Member; import com.cruru.member.domain.repository.MemberRepository; @@ -25,6 +26,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; @DisplayName("인증 컨트롤러 테스트") class AuthControllerTest extends ControllerTest { @@ -35,6 +37,9 @@ class AuthControllerTest extends ControllerTest { @Autowired private ClubRepository clubRepository; + @MockBean + private TokenRedisClient tokenRedisClient; + private Member member; @BeforeEach @@ -50,6 +55,7 @@ void setup() { void login() { // given LoginRequest request = new LoginRequest(member.getEmail(), "qwer1234"); + doNothing().when(tokenRedisClient).saveToken(any(), any()); // when&then RestAssured.given(spec).log().all() diff --git a/backend/src/test/java/com/cruru/auth/domain/repository/RefreshTokenRepositoryTest.java b/backend/src/test/java/com/cruru/auth/domain/repository/RefreshTokenRepositoryTest.java deleted file mode 100644 index 6ced43cb8..000000000 --- a/backend/src/test/java/com/cruru/auth/domain/repository/RefreshTokenRepositoryTest.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.cruru.auth.domain.repository; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.cruru.auth.domain.RefreshToken; -import com.cruru.member.domain.Member; -import com.cruru.member.domain.repository.MemberRepository; -import com.cruru.util.RepositoryTest; -import com.cruru.util.fixture.MemberFixture; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -@DisplayName("리프레쉬 토큰 레포지터리 테스트") -class RefreshTokenRepositoryTest extends RepositoryTest { - - @Autowired - private RefreshTokenRepository refreshTokenRepository; - - @Autowired - private MemberRepository memberRepository; - - private Member member; - - @BeforeEach - void setUp() { - refreshTokenRepository.deleteAllInBatch(); - memberRepository.deleteAllInBatch(); - - member = memberRepository.save(MemberFixture.DOBBY); - } - - @DisplayName("이미 DB에 저장되어 있는 ID를 가진 토큰을 저장하면, 해당 ID의 토큰은 후에 작성된 정보로 업데이트한다.") - @Test - void sameIdUpdate() { - //given - String token = "token"; - RefreshToken saved = refreshTokenRepository.save(new RefreshToken(token, member)); - - //when - String changedToken = "changedToken"; - RefreshToken refreshToken = new RefreshToken(saved.getId(), changedToken, null); - refreshTokenRepository.save(refreshToken); - - //then - RefreshToken foundToken = refreshTokenRepository.findById(saved.getId()).get(); - assertThat(foundToken.getToken()).isEqualTo(changedToken); - } - - @DisplayName("ID가 없는 사용자를 저장하면, ID를 순차적으로 부여하여 저장한다.") - @Test - void saveNoId() { - //given - Member member1 = memberRepository.save(MemberFixture.RUSH); - RefreshToken refreshToken1 = new RefreshToken("token", member); - RefreshToken refreshToken2 = new RefreshToken("token2", member1); - - //when - RefreshToken savedrefreshToken1 = refreshTokenRepository.save(refreshToken1); - RefreshToken savedrefreshToken2 = refreshTokenRepository.save(refreshToken2); - - //then - assertThat(savedrefreshToken1.getId() + 1).isEqualTo(savedrefreshToken2.getId()); - } -} diff --git a/backend/src/test/java/com/cruru/auth/service/AuthServiceTest.java b/backend/src/test/java/com/cruru/auth/service/AuthServiceTest.java index 50bd48549..ceb93c074 100644 --- a/backend/src/test/java/com/cruru/auth/service/AuthServiceTest.java +++ b/backend/src/test/java/com/cruru/auth/service/AuthServiceTest.java @@ -3,12 +3,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.doReturn; import com.cruru.auth.controller.response.TokenResponse; import com.cruru.auth.domain.AccessToken; -import com.cruru.auth.domain.RefreshToken; import com.cruru.auth.domain.Token; -import com.cruru.auth.domain.repository.RefreshTokenRepository; import com.cruru.auth.exception.IllegalTokenException; import com.cruru.member.domain.Member; import com.cruru.member.domain.repository.MemberRepository; @@ -19,10 +18,12 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; @DisplayName("AuthService 테스트") class AuthServiceTest extends ServiceTest { @@ -37,8 +38,8 @@ class AuthServiceTest extends ServiceTest { @Autowired private MemberRepository memberRepository; - @Autowired - private RefreshTokenRepository refreshTokenRepository; + @MockBean + private TokenRedisClient tokenRedisClient; private Member member; @@ -56,7 +57,7 @@ void setUp() { @Test void createAccessToken() { // given&when - Token accessToken = authService.createAccessToken(member); + Token accessToken = authService.createAccessToken(member.getEmail(), member.getRole()); // then assertAll( @@ -69,7 +70,7 @@ void createAccessToken() { @Test void isTokenSignatureValid() { // given - Token accessToken = authService.createAccessToken(member); + Token accessToken = authService.createAccessToken(member.getEmail(), member.getRole()); // when boolean isValid = authService.isTokenSignatureValid(accessToken.getToken()); @@ -95,7 +96,7 @@ void isTokenInvalid() { @Test void extractEmail() { // given - Token accessToken = authService.createAccessToken(member); + Token accessToken = authService.createAccessToken(member.getEmail(), member.getRole()); // when String email = authService.extractEmail(accessToken.getToken()); @@ -108,7 +109,7 @@ void extractEmail() { @Test void extractRole() { // given - Token accessToken = authService.createAccessToken(member); + Token accessToken = authService.createAccessToken(member.getEmail(), member.getRole()); // when String role = authService.extractMemberRole(accessToken.getToken()); @@ -146,14 +147,17 @@ void isNotVerifiedPassword() { @Test void refresh() throws InterruptedException { // given - Token refreshToken = refreshTokenRepository.save((RefreshToken) authService.createRefreshToken(member)); + Token refreshToken = authService.createRefreshToken(member.getEmail(), member.getRole()); + tokenRedisClient.saveToken(member.getEmail(), refreshToken.getToken()); + doReturn(true).when(tokenRedisClient).existsByToken(member.getEmail(), refreshToken.getToken()); + doReturn(Optional.of(refreshToken.getToken())).when(tokenRedisClient).getToken(member.getEmail()); // when Thread.sleep(1000); TokenResponse tokenResponse = authService.refresh(refreshToken.getToken()); // then - assertThat(refreshToken.getToken()).isNotEqualTo(tokenResponse.refreshToken()); + assertThat(tokenResponse.refreshToken()).isNotEqualTo(refreshToken.getToken()); } @DisplayName("토큰의 만료 여부를 검증한다.") diff --git a/backend/src/test/java/com/cruru/dashboard/facade/DashboardFacadeTest.java b/backend/src/test/java/com/cruru/dashboard/facade/DashboardFacadeTest.java index cd024de39..bf473e54e 100644 --- a/backend/src/test/java/com/cruru/dashboard/facade/DashboardFacadeTest.java +++ b/backend/src/test/java/com/cruru/dashboard/facade/DashboardFacadeTest.java @@ -165,7 +165,7 @@ void findAllDashboardsByClubId() { () -> assertThat(dashboardsOfClubResponse.clubName()).isEqualTo(club.getName()), () -> assertThat(dashboardPreview.dashboardId()).isEqualTo(dashboard.getId()), () -> assertThat(dashboardPreview.title()).isEqualTo(applyForm.getTitle()), - () -> assertThat(dashboardPreview.applyFormId()).isEqualTo(applyForm.getId()), + () -> assertThat(dashboardPreview.applyFormId()).isEqualTo(String.valueOf(applyForm.getId())), () -> assertThat(dashboardPreview.endDate()).isEqualTo(applyForm.getEndDate()), () -> assertThat(stats.accept()).isEqualTo(1), () -> assertThat(stats.fail()).isEqualTo(2), diff --git a/backend/src/test/java/com/cruru/email/controller/EmailControllerTest.java b/backend/src/test/java/com/cruru/email/controller/EmailControllerTest.java index aaa4026a0..d85fc7489 100644 --- a/backend/src/test/java/com/cruru/email/controller/EmailControllerTest.java +++ b/backend/src/test/java/com/cruru/email/controller/EmailControllerTest.java @@ -1,22 +1,32 @@ package com.cruru.email.controller; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.request.RequestDocumentation.partWithName; import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; import com.cruru.applicant.domain.Applicant; import com.cruru.applicant.domain.repository.ApplicantRepository; +import com.cruru.email.controller.request.SendVerificationCodeRequest; +import com.cruru.email.controller.request.VerifyCodeRequest; +import com.cruru.email.service.EmailRedisClient; +import com.cruru.member.domain.repository.MemberRepository; import com.cruru.util.ControllerTest; import com.cruru.util.fixture.ApplicantFixture; import com.cruru.util.fixture.EmailFixture; +import com.cruru.util.fixture.MemberFixture; import io.restassured.RestAssured; import io.restassured.http.ContentType; import java.io.File; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; @DisplayName("이메일 컨트롤러 테스트") class EmailControllerTest extends ControllerTest { @@ -24,6 +34,12 @@ class EmailControllerTest extends ControllerTest { @Autowired private ApplicantRepository applicantRepository; + @Autowired + private MemberRepository memberRepository; + + @MockBean + private EmailRedisClient emailRedisClient; + @DisplayName("이메일 발송 성공 시, 200을 응답한다.") @Test void send() { @@ -121,4 +137,155 @@ void send_clubNotExist() { .when().post("/v1/emails/send") .then().log().all().statusCode(404); } + + @DisplayName("이메일 인증 번호 발송 성공 시, 200을 응답한다.") + @Test + void sendVerificationCode() { + // given + SendVerificationCodeRequest request = new SendVerificationCodeRequest("email@email.com"); + + // when & then + RestAssured.given(spec).log().all() + .contentType(ContentType.JSON) + .body(request) + .filter(document("email/verification-code", + requestFields(fieldWithPath("email").description("인증 번호를 전송할 이메일")) + )) + .when().post("/v1/emails/verification-code") + .then().log().all().statusCode(200); + } + + @DisplayName("이메일 인증 번호 발송 시, 이미 가입된 이메일일 경우 400을 응답한다.") + @Test + void sendVerificationCode_alreadySignedUp() { + // given + String email = MemberFixture.DOBBY.getEmail(); + memberRepository.save(MemberFixture.DOBBY); + SendVerificationCodeRequest request = new SendVerificationCodeRequest(email); + + // when & then + RestAssured.given(spec).log().all() + .contentType(ContentType.JSON) + .body(request) + .filter(document("email/verification-code-fail/already-signed-up", + requestFields(fieldWithPath("email").description("이미 가입된 이메일")) + )) + .when().post("/v1/emails/verification-code") + .then().log().all().statusCode(409); + } + + @DisplayName("이메일 인증 번호 발송 시, 이메일 형식이 올바르지 않을 경우 400을 응답한다.") + @Test + void sendVerificationCode_invalidEmailFormat() { + // given + String email = "invalidEmail"; + SendVerificationCodeRequest request = new SendVerificationCodeRequest(email); + + // when & then + RestAssured.given(spec).log().all() + .contentType(ContentType.JSON) + .body(request) + .filter(document("email/verification-code-fail/invalid-email", + requestFields(fieldWithPath("email").description("부적절한 이메일")) + )) + .when().post("/v1/emails/verification-code") + .then().log().all().statusCode(400); + } + + @DisplayName("이메일 인증 성공 시, 200을 응답한다.") + @Test + void verifyCode() { + // given + String email = "email@email.com"; + String verificationCode = "123456"; + + doNothing().when(emailRedisClient).saveVerificationCode(email, verificationCode); + when(emailRedisClient.getVerificationCode(email)).thenReturn(verificationCode); + + VerifyCodeRequest request = new VerifyCodeRequest(email, verificationCode); + + // when & then + RestAssured.given(spec).log().all() + .contentType(ContentType.JSON) + .body(request) + .filter(document("email/verify-code", + requestFields( + fieldWithPath("email").description("인증할 이메일"), + fieldWithPath("verificationCode").description("인증 번호") + ) + )) + .when().post("/v1/emails/verify-code") + .then().log().all().statusCode(200); + } + + @DisplayName("이메일 인증 시, 이메일 형식이 올바르지 않을 경우 400을 응답한다.") + @Test + void verifyCode_invalidEmailFormat() { + // given + String email = "invalidEmail"; + String verificationCode = "123456"; + + emailRedisClient.saveVerificationCode(email, verificationCode); + VerifyCodeRequest request = new VerifyCodeRequest(email, verificationCode); + + // when & then + RestAssured.given(spec).log().all() + .contentType(ContentType.JSON) + .body(request) + .filter(document("email/verify-code-fail/invalid-email", + requestFields( + fieldWithPath("email").description("부적절한 이메일 형식"), + fieldWithPath("verificationCode").description("인증 번호")) + )) + .when().post("/v1/emails/verify-code") + .then().log().all().statusCode(400); + } + + @DisplayName("이메일 인증 시, 인증 번호가 없는 이메일일 경우 400을 응답한다.") + @Test + void verifyCode_codeNotFound() { + // given + String email = "email@email.com"; + String invalidEmail = "no-code@mail.com"; + String verificationCode = "123456"; + + emailRedisClient.saveVerificationCode(email, verificationCode); + VerifyCodeRequest request = new VerifyCodeRequest(invalidEmail, verificationCode); + + // when & then + RestAssured.given(spec).log().all() + .contentType(ContentType.JSON) + .body(request) + .filter(document("email/verify-code-fail/code-not-found", + requestFields( + fieldWithPath("email").description("인증 번호가 없는 이메일"), + fieldWithPath("verificationCode").description("인증 번호")) + )) + .when().post("/v1/emails/verify-code") + .then().log().all().statusCode(400); + } + + @DisplayName("이메일 인증 시, 인증 번호가 다른 이메일일 경우 400을 응답한다.") + @Test + void verifyCode_codeMisMatch() { + // given + String email = "email@email.com"; + String verificationCode = "123456"; + String invalidVerificationCode = "654321"; + + emailRedisClient.saveVerificationCode(email, verificationCode); + VerifyCodeRequest request = new VerifyCodeRequest(email, invalidVerificationCode); + + // when & then + RestAssured.given(spec).log().all() + .contentType(ContentType.JSON) + .body(request) + .filter(document("email/verify-code-fail/code-mismatch", + requestFields( + fieldWithPath("email").description("인증할 이메일"), + fieldWithPath("verificationCode").description("적절하지 않은 인증 번호")) + )) + .when().post("/v1/emails/verify-code") + .then().log().all().statusCode(400); + } } diff --git a/backend/src/test/java/com/cruru/email/facade/EmailFacadeTest.java b/backend/src/test/java/com/cruru/email/facade/EmailFacadeTest.java index 6c451593c..4f0944b1b 100644 --- a/backend/src/test/java/com/cruru/email/facade/EmailFacadeTest.java +++ b/backend/src/test/java/com/cruru/email/facade/EmailFacadeTest.java @@ -1,18 +1,28 @@ package com.cruru.email.facade; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.awaitility.Awaitility.await; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import com.cruru.applicant.domain.Applicant; import com.cruru.applicant.domain.repository.ApplicantRepository; -import com.cruru.email.controller.dto.EmailRequest; +import com.cruru.email.controller.request.EmailRequest; +import com.cruru.email.controller.request.SendVerificationCodeRequest; +import com.cruru.email.controller.request.VerifyCodeRequest; import com.cruru.email.domain.Email; +import com.cruru.email.exception.EmailConflictException; +import com.cruru.email.exception.badrequest.VerificationCodeMismatchException; +import com.cruru.email.exception.badrequest.VerificationCodeNotFoundException; +import com.cruru.email.service.EmailRedisClient; import com.cruru.email.service.EmailService; +import com.cruru.member.domain.repository.MemberRepository; import com.cruru.util.ServiceTest; import com.cruru.util.fixture.ApplicantFixture; import com.cruru.util.fixture.EmailFixture; +import com.cruru.util.fixture.MemberFixture; import jakarta.mail.internet.MimeMessage; import java.util.List; import java.util.concurrent.TimeUnit; @@ -20,6 +30,7 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; @DisplayName("발송 내역 파사드 테스트") @@ -27,11 +38,19 @@ class EmailFacadeTest extends ServiceTest { @SpyBean EmailService emailService; + @Autowired private ApplicantRepository applicantRepository; + + @Autowired + private MemberRepository memberRepository; + @Autowired private EmailFacade emailFacade; + @MockBean + private EmailRedisClient emailRedisClient; + @DisplayName("이메일을 비동기로 발송하고, 발송 내역을 저장한다.") @Test void sendAndSave() { @@ -60,4 +79,67 @@ void sendAndSave() { verify(emailService, times(1)).save(any(Email.class)); }); } + + @DisplayName("이미 가입된 이메일로 인증을 시도하면 예외가 발생한다.") + @Test + void sendVerificationCode() { + // given + String email = MemberFixture.DOBBY.getEmail(); + memberRepository.save(MemberFixture.DOBBY); + SendVerificationCodeRequest request = new SendVerificationCodeRequest(email); + + // when&then + assertThatThrownBy(() -> emailFacade.sendVerificationCode(request)) + .isInstanceOf(EmailConflictException.class); + } + + @DisplayName("이메일 인증 성공 시, 인증 코드가 검증된다.") + @Test + void verifyCode() { + // given + String email = "test@example.com"; + String verificationCode = "123456"; + VerifyCodeRequest request = new VerifyCodeRequest(email, verificationCode); + + when(emailRedisClient.getVerificationCode(email)).thenReturn(verificationCode); + + // when + emailFacade.verifyCode(request); + + // then + verify(emailRedisClient, times(1)).getVerificationCode(email); + } + + @DisplayName("저장된 인증 코드가 없으면 예외가 발생한다.") + @Test + void verifyCode_verificationCodeNotFoundException() { + // given + String email = "test@example.com"; + String verificationCode = "123456"; + VerifyCodeRequest request = new VerifyCodeRequest(email, verificationCode); + + when(emailRedisClient.getVerificationCode(email)).thenReturn(null); + + // when&then + assertThatThrownBy(() -> emailFacade.verifyCode(request)) + .isInstanceOf(VerificationCodeNotFoundException.class) + .hasMessage("인증 코드가 존재하지 않거나 만료되었습니다."); + } + + @DisplayName("저장된 인증 코드와 일치하지 않으면 예외가 발생한다.") + @Test + void verifyCode_verificationCodeMismatchException() { + // given + String email = "test@example.com"; + String correctCode = "123456"; + String wrongCode = "654321"; + VerifyCodeRequest request = new VerifyCodeRequest(email, wrongCode); + + when(emailRedisClient.getVerificationCode(email)).thenReturn(correctCode); + + // when&then + assertThatThrownBy(() -> emailFacade.verifyCode(request)) + .isInstanceOf(VerificationCodeMismatchException.class) + .hasMessage("인증 코드가 일치하지 않습니다."); + } } diff --git a/backend/src/test/java/com/cruru/member/controller/MemberControllerTest.java b/backend/src/test/java/com/cruru/member/controller/MemberControllerTest.java index 712190a00..60627962a 100644 --- a/backend/src/test/java/com/cruru/member/controller/MemberControllerTest.java +++ b/backend/src/test/java/com/cruru/member/controller/MemberControllerTest.java @@ -1,9 +1,13 @@ package com.cruru.member.controller; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document; +import com.cruru.email.exception.NotVerifiedEmailException; +import com.cruru.email.service.EmailRedisClient; import com.cruru.member.controller.request.MemberCreateRequest; import com.cruru.util.ControllerTest; import io.restassured.RestAssured; @@ -13,10 +17,14 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.test.mock.mockito.MockBean; @DisplayName("사용자 컨트롤러 테스트") class MemberControllerTest extends ControllerTest { + @MockBean + private EmailRedisClient emailRedisClient; + private static Stream InvalidMemberSignUpRequest() { String validName = "크루루"; String validMail = "mail@mail.com"; @@ -40,6 +48,7 @@ private static Stream InvalidMemberSignUpRequest() { void create() { // given MemberCreateRequest request = new MemberCreateRequest("크루루", "mail@mail.com", "newPassword214!", "01012341234"); + doNothing().when(emailRedisClient).verifyEmail(request.email()); // when&then RestAssured.given(spec).log().all() @@ -61,7 +70,10 @@ void create() { @ParameterizedTest @MethodSource("InvalidMemberSignUpRequest") void create_invalidEmail(MemberCreateRequest request) { - // given&when&then + // given + doNothing().when(emailRedisClient).verifyEmail(request.email()); + + //when&then RestAssured.given(spec).log().all() .contentType(ContentType.JSON) .body(request) @@ -76,4 +88,27 @@ void create_invalidEmail(MemberCreateRequest request) { .when().post("/v1/members/signup") .then().log().all().statusCode(400); } + + @DisplayName("인증되지 않은 사용자가 회원가입할 경우, 401을 반환한다.") + @Test + void create_notVerifiedEmail() { + // given + MemberCreateRequest request = new MemberCreateRequest("크루루", "mail@mail.com", "newPassword214!", "01012341234"); + doThrow(NotVerifiedEmailException.class).when(emailRedisClient).verifyEmail(request.email()); + + // when&then + RestAssured.given(spec).log().all() + .contentType(ContentType.JSON) + .body(request) + .filter(document("member/signup-fail/not-verified-email", + requestFields( + fieldWithPath("clubName").description("동아리명"), + fieldWithPath("email").description("인증되지 않은 사용자 이메일"), + fieldWithPath("password").description("사용자 패스워드"), + fieldWithPath("phone").description("사용자 전화번호") + ) + )) + .when().post("/v1/members/signup") + .then().log().all().statusCode(401); + } } diff --git a/backend/src/test/java/com/cruru/member/facade/MemberFacadeTest.java b/backend/src/test/java/com/cruru/member/facade/MemberFacadeTest.java index aa00e698d..b472db0df 100644 --- a/backend/src/test/java/com/cruru/member/facade/MemberFacadeTest.java +++ b/backend/src/test/java/com/cruru/member/facade/MemberFacadeTest.java @@ -1,8 +1,13 @@ package com.cruru.member.facade; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import com.cruru.email.exception.NotVerifiedEmailException; +import com.cruru.email.service.EmailRedisClient; import com.cruru.member.controller.request.MemberCreateRequest; import com.cruru.member.domain.Member; import com.cruru.member.domain.repository.MemberRepository; @@ -11,6 +16,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; @DisplayName("회원 파사드 서비스 테스트") class MemberFacadeTest extends ServiceTest { @@ -21,6 +27,9 @@ class MemberFacadeTest extends ServiceTest { @Autowired private MemberRepository memberRepository; + @MockBean + private EmailRedisClient emailRedisClient; + @DisplayName("사용자를 생성하면 ID를 반환한다.") @Test void create() { @@ -30,6 +39,7 @@ void create() { String password = "newPassword214!"; String phone = "01012341234"; MemberCreateRequest request = new MemberCreateRequest(clubName, email, password, phone); + doNothing().when(emailRedisClient).verifyEmail(email); // when long memberId = memberFacade.create(request); @@ -42,4 +52,20 @@ void create() { () -> assertThat(member.get().getPhone()).isEqualTo(phone) ); } + + @DisplayName("인증되지 않은 사용자로 사용자를 생성하면, 예외가 발생한다.") + @Test + void create_notVerifiedEmail() { + // given + String clubName = "크루루"; + String email = "new@mail.com"; + String password = "newPassword214!"; + String phone = "01012341234"; + MemberCreateRequest request = new MemberCreateRequest(clubName, email, password, phone); + doThrow(NotVerifiedEmailException.class).when(emailRedisClient).verifyEmail(email); + + // when&then + assertThatThrownBy(() -> memberFacade.create(request)) + .isInstanceOf(NotVerifiedEmailException.class); + } } diff --git a/backend/src/test/java/com/cruru/process/facade/ProcessFacadeTest.java b/backend/src/test/java/com/cruru/process/facade/ProcessFacadeTest.java index 18320d432..493fcd801 100644 --- a/backend/src/test/java/com/cruru/process/facade/ProcessFacadeTest.java +++ b/backend/src/test/java/com/cruru/process/facade/ProcessFacadeTest.java @@ -87,14 +87,14 @@ void readAllByDashboardId() { evaluationRepository.saveAll(evaluations1); List evaluations2 = List.of( - EvaluationFixture.fivePoints(process2, applicant2), - EvaluationFixture.fivePoints(process2, applicant2), - EvaluationFixture.fivePoints(process2, applicant2), - EvaluationFixture.fourPoints(process2, applicant2) + EvaluationFixture.fivePoints(process1, applicant2), + EvaluationFixture.fivePoints(process1, applicant2), + EvaluationFixture.fivePoints(process1, applicant2), + EvaluationFixture.fourPoints(process1, applicant2) ); evaluationRepository.saveAll(evaluations2); - String sortByCreatedAt = "DESC"; - String sortByScore = null; + String sortByCreatedAt = null; + String sortByScore = "DESC"; // when ProcessResponses processResponses = processFacade.readAllByDashboardId( @@ -114,7 +114,7 @@ void readAllByDashboardId() { () -> assertThat(processResponses.processResponses()).hasSize(2), () -> assertThat(processId).isEqualTo(process1.getId()), () -> assertThat(applicantCardResponse.id()).isEqualTo(applicant2.getId()), - () -> assertThat(applicantCardResponse.evaluationCount()).isEqualTo(evaluations1.size()), + () -> assertThat(applicantCardResponse.evaluationCount()).isEqualTo(evaluations2.size()), () -> assertThat(applicantCardResponse.averageScore()).isEqualTo(4.75) ); } diff --git a/backend/src/test/java/com/cruru/question/controller/QuestionControllerTest.java b/backend/src/test/java/com/cruru/question/controller/QuestionControllerTest.java index 0a12abcea..1a9b99e63 100644 --- a/backend/src/test/java/com/cruru/question/controller/QuestionControllerTest.java +++ b/backend/src/test/java/com/cruru/question/controller/QuestionControllerTest.java @@ -50,7 +50,7 @@ class QuestionControllerTest extends ControllerTest { @DisplayName("존재하는 질문의 변경 성공시, 200을 응답한다.") @Test - void update() { + void updateById() { // given ApplyForm applyForm = applyFormRepository.save(ApplyFormFixture.notStarted()); questionRepository.save(QuestionFixture.multipleChoiceType(applyForm)); @@ -71,7 +71,7 @@ void update() { .cookie("accessToken", token) .contentType(ContentType.JSON) .body(questionUpdateRequests) - .filter(document("question/update", + .filter(document("question/update-id", requestCookies(cookieWithName("accessToken").description("사용자 토큰")), queryParameters(parameterWithName("applyformId").description("질문을 변경할 지원폼의 id")), requestFields(fieldWithPath("questions").description("변경할 질문들")) @@ -86,7 +86,7 @@ void update() { @Test void update_applyFormNotFound() { // given - Long invalidApplyFormId = -1L; + long invalidApplyFormId = -1; ApplyForm applyForm = applyFormRepository.save(ApplyFormFixture.notStarted()); questionRepository.save(QuestionFixture.multipleChoiceType(applyForm)); QuestionUpdateRequests questionUpdateRequests = new QuestionUpdateRequests( diff --git a/backend/src/test/java/com/cruru/question/facade/QuestionFacadeTest.java b/backend/src/test/java/com/cruru/question/facade/QuestionFacadeTest.java index 4e670c0f3..256dff7ac 100644 --- a/backend/src/test/java/com/cruru/question/facade/QuestionFacadeTest.java +++ b/backend/src/test/java/com/cruru/question/facade/QuestionFacadeTest.java @@ -40,7 +40,52 @@ class QuestionFacadeTest extends ServiceTest { @DisplayName("질문을 수정한다.") @Test - void update() { + void updateById() { + // given + ApplyForm applyForm = applyFormRepository.save(ApplyFormFixture.notStarted()); + Question question = questionRepository.save(QuestionFixture.multipleChoiceType(applyForm)); + choiceRepository.save(ChoiceFixture.first(question)); + choiceRepository.save(ChoiceFixture.second(question)); + + Question newQuestion = QuestionFixture.singleChoiceType(applyForm); + Choice newChoice = ChoiceFixture.third(question); + + QuestionUpdateRequests questionUpdateRequest = new QuestionUpdateRequests(List.of( + new QuestionCreateRequest( + newQuestion.getQuestionType().name(), + newQuestion.getContent(), + List.of(new ChoiceCreateRequest( + newChoice.getContent(), + newChoice.getSequence() + )), + newQuestion.getSequence(), + newQuestion.isRequired() + ))); + + // when + questionFacade.update(questionUpdateRequest, applyForm.getId()); + + // then + List actualQuestions = questionRepository.findAllByApplyForm(applyForm); + Question actualQuestion = actualQuestions.get(0); + List actualChoices = choiceRepository.findAllByQuestion(actualQuestion); + Choice actualChoice = actualChoices.get(0); + assertAll( + () -> assertThat(actualQuestions).hasSize(1), + () -> assertThat(actualQuestion.getQuestionType()).isEqualTo(newQuestion.getQuestionType()), + () -> assertThat(actualQuestion.getContent()).isEqualTo(newQuestion.getContent()), + () -> assertThat(actualQuestion.getSequence()).isEqualTo(newQuestion.getSequence()), + () -> assertThat(actualQuestion.isRequired()).isEqualTo(newQuestion.isRequired()), + + () -> assertThat(actualChoices).hasSize(1), + () -> assertThat(actualChoice.getContent()).isEqualTo(newChoice.getContent()), + () -> assertThat(actualChoice.getSequence()).isEqualTo(newChoice.getSequence()) + ); + } + + @DisplayName("질문을 수정한다.") + @Test + void updateByTsid() { // given ApplyForm applyForm = applyFormRepository.save(ApplyFormFixture.notStarted()); Question question = questionRepository.save(QuestionFixture.multipleChoiceType(applyForm)); diff --git a/backend/src/test/java/com/cruru/util/ControllerTest.java b/backend/src/test/java/com/cruru/util/ControllerTest.java index 7eaa2a273..a6eb0d18c 100644 --- a/backend/src/test/java/com/cruru/util/ControllerTest.java +++ b/backend/src/test/java/com/cruru/util/ControllerTest.java @@ -66,7 +66,7 @@ void createDefaultLoginMember() { dbCleaner.truncateEveryTable(); defaultMember = memberRepository.save(MemberFixture.ADMIN); defaultClub = clubRepository.save(ClubFixture.create(defaultMember)); - token = authService.createAccessToken(defaultMember).getToken(); + token = authService.createAccessToken(defaultMember.getEmail(), defaultMember.getRole()).getToken(); } @BeforeEach