From 907708d5328c57532fd6550073a76b2465bbf950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EB=8F=99=ED=98=84?= Date: Wed, 1 Nov 2023 15:37:43 +0900 Subject: [PATCH] =?UTF-8?q?[Refactor]=20=EC=A0=84=EC=B2=B4=EC=A0=81?= =?UTF-8?q?=EC=9D=B8=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81=20(#44)=20=20(#45)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: api 재활용을 위한 코드 수정 (#44) * refactor: api 재활용을 위한 코드 수정 (#44) * feat: userService test (#44) * test: 검증이 빠진 부분 검증 (#44) * test: 이메일로 지원 시작 알림 테스트 (#44) * test: 사진 업로드 테스트 (#44) * test: 사진 업로드 테스트 (#44) * test: 사진 업로드 테스트 (#44) * test: 이메일 테스트 (#44) * refactor: cors 설정 변경 (#44) * refactor: cors 설정 변경 (#44) * test: cors test (#44) * test: user error test (#44) * test: apply period test (#44) * test: apply period test (#44) * test: apply service test (#44) * test: user test (#44) * test: test code (#44) * refactor: delete unused class (#44) * test: exception handler test (#44) * refactor: 지원서 리팩토링 (#44) * refactor: config 리팩토링 (#44) * refactor: email 리팩토링 (#44) * refactor: user 리팩토링 (#44) * refactor: response 리팩토링 (#44) * refactor: controller 제네릭 명시 (#44) --- .gitignore | 2 + build.gradle | 2 - .../asciidoc/api/application/application.adoc | 18 +- src/docs/asciidoc/api/email/email.adoc | 3 + src/docs/asciidoc/index.adoc | 4 + .../controller/ApplicationController.java | 152 +++-------- .../apply/dto/request/ResultNotification.java | 17 ++ .../dto/response/SingleApplicationResult.java | 13 - .../apply/service/ApplyPeriodService.java | 28 +- .../server/apply/service/ApplyService.java | 66 ++--- .../yonseigolf/server/config/CorsConfig.java | 37 ++- .../email/controller/EmailController.java | 10 +- .../server/email/dto/NotificationType.java | 37 +++ .../server/email/service/EmailService.java | 13 +- .../user/controller/UserController.java | 69 ++--- .../controller/UserExceptionController.java | 11 +- .../server/user/dto/request/KakaoCode.java | 2 +- .../server/user/dto/response/SessionUser.java | 4 +- .../user/service/OauthLoginService.java | 27 +- .../server/user/service/UserService.java | 2 +- .../yonseigolf/server/util/AesEncryptor.java | 82 ------ .../server/util/CustomResponse.java | 21 ++ .../server/ServerApplicationTests.java | 3 +- .../controller/ApplicationControllerTest.java | 108 ++++---- .../server/apply/image/ImageServiceTest.java | 68 +++++ .../service/ApplicationPeriodServiceTest.java | 136 ++++++++++ .../apply/service/ApplyServiceTest.java | 240 ++++++++++++++++++ .../server/config/CorsConfigTest.java | 63 +++++ .../email/controller/EmailControllerTest.java | 49 ++++ .../email/dto/NotificationTypeTest.java | 49 ++++ .../email/service/EmailServiceTest.java | 72 ++++++ .../user/controller/UserControllerTest.java | 52 +++- .../UserIntegrateControllerTest.java | 86 +++++++ .../user/service/OauthLoginServiceTest.java | 84 ++++++ .../server/user/service/UserServiceTest.java | 196 ++++++++++++++ src/test/resources/application-test.yml | 10 + 36 files changed, 1394 insertions(+), 442 deletions(-) create mode 100644 src/docs/asciidoc/api/email/email.adoc create mode 100644 src/main/java/yonseigolf/server/apply/dto/request/ResultNotification.java create mode 100644 src/main/java/yonseigolf/server/email/dto/NotificationType.java delete mode 100644 src/main/java/yonseigolf/server/util/AesEncryptor.java create mode 100644 src/test/java/yonseigolf/server/apply/image/ImageServiceTest.java create mode 100644 src/test/java/yonseigolf/server/apply/service/ApplicationPeriodServiceTest.java create mode 100644 src/test/java/yonseigolf/server/apply/service/ApplyServiceTest.java create mode 100644 src/test/java/yonseigolf/server/config/CorsConfigTest.java create mode 100644 src/test/java/yonseigolf/server/email/controller/EmailControllerTest.java create mode 100644 src/test/java/yonseigolf/server/email/dto/NotificationTypeTest.java create mode 100644 src/test/java/yonseigolf/server/email/service/EmailServiceTest.java create mode 100644 src/test/java/yonseigolf/server/user/controller/UserIntegrateControllerTest.java create mode 100644 src/test/java/yonseigolf/server/user/service/OauthLoginServiceTest.java create mode 100644 src/test/java/yonseigolf/server/user/service/UserServiceTest.java create mode 100644 src/test/resources/application-test.yml diff --git a/.gitignore b/.gitignore index 825cf1d..f5e2b86 100644 --- a/.gitignore +++ b/.gitignore @@ -98,3 +98,5 @@ out daemon bin .jqwik-database + +lombok.config \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4b3af44..0eeced6 100644 --- a/build.gradle +++ b/build.gradle @@ -53,8 +53,6 @@ dependencies { testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' // aws service - implementation 'com.amazonaws:aws-java-sdk-core:1.12.429' - implementation 'com.amazonaws:aws-java-sdk-s3:1.12.429' implementation 'software.amazon.awssdk:s3:2.16.83' // querydsl 추가 diff --git a/src/docs/asciidoc/api/application/application.adoc b/src/docs/asciidoc/api/application/application.adoc index 47c5f3e..9b88089 100644 --- a/src/docs/asciidoc/api/application/application.adoc +++ b/src/docs/asciidoc/api/application/application.adoc @@ -56,19 +56,7 @@ include::{snippets}/admin-application-updateDocumentPass-doc/request-fields.adoc include::{snippets}/admin-application-updateInterviewTime-doc/http-request.adoc[] include::{snippets}/admin-application-updateInterviewTime-doc/request-fields.adoc[] - -=== 서류 합격 메일 -==== HTTP Request -include::{snippets}/admin-application-sendDocumentPassEmail-doc/http-request.adoc[] - -=== 서류 불합격 메일 -==== HTTP Request -include::{snippets}/admin-application-sendDocumentFailEmail-doc/http-request.adoc[] - -=== 최종 합격 메일 -==== HTTP Request -include::{snippets}/admin-application-sendFinalPassEmail-doc/http-request.adoc[] - -=== 최종 불합격 메일 +=== 지원 결과 이메일 전송 ==== HTTP Request -include::{snippets}/admin-application-sendFinalFailEmail-doc/http-request.adoc[] \ No newline at end of file +include::{snippets}/admin-application-sendResult-doc/http-request.adoc[] +include::{snippets}/admin-application-sendResult-doc/request-fields.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/api/email/email.adoc b/src/docs/asciidoc/api/email/email.adoc new file mode 100644 index 0000000..954b630 --- /dev/null +++ b/src/docs/asciidoc/api/email/email.adoc @@ -0,0 +1,3 @@ +=== 지원 기간 시작 시 이메일 전송 +==== HTTP Request +include::{snippets}/email-apply-start-email/http-request.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index afdeb48..8d34f9b 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -22,3 +22,7 @@ include::api/application/application.adoc[] == User API include::api/users/users.adoc[] + +[[Email-API]] +== Email API +include::api/email/email.adoc[] diff --git a/src/main/java/yonseigolf/server/apply/controller/ApplicationController.java b/src/main/java/yonseigolf/server/apply/controller/ApplicationController.java index 1548649..33b2b79 100644 --- a/src/main/java/yonseigolf/server/apply/controller/ApplicationController.java +++ b/src/main/java/yonseigolf/server/apply/controller/ApplicationController.java @@ -11,6 +11,7 @@ import yonseigolf.server.apply.dto.request.*; import yonseigolf.server.apply.dto.response.ApplicationResponse; import yonseigolf.server.apply.dto.response.ImageResponse; +import yonseigolf.server.apply.dto.response.RecruitPeriodResponse; import yonseigolf.server.apply.dto.response.SingleApplicationResult; import yonseigolf.server.apply.image.ImageService; import yonseigolf.server.apply.service.ApplyPeriodService; @@ -35,75 +36,59 @@ public ApplicationController(ApplyService applicationService, ApplyPeriodService } @PostMapping("/application") - public ResponseEntity apply(@RequestBody ApplicationRequest request) { + public ResponseEntity> apply(@RequestBody ApplicationRequest request) { applicationService.apply(request); return ResponseEntity .ok() - .body(new CustomResponse( - "success", - 200, - "연세골프 지원서 제출 성공" - )); + .body(CustomResponse.successResponse("연세골프 지원서 제출 성공")); } @PostMapping("/application/emailAlarm") - public ResponseEntity emailAlarm(@RequestBody EmailAlertRequest request) { + public ResponseEntity> emailAlarm(@RequestBody EmailAlertRequest request) { applicationService.emailAlarm(request); return ResponseEntity .ok() - .body(new CustomResponse( - "success", - 200, - "연세골프 이메일 알림 신청 성공" - )); + .body(CustomResponse.successResponse("연세골프 지원서 이메일 알림 설정 성공")); } @GetMapping("/application/recruit") - public ResponseEntity getApplicationPeriod() { + public ResponseEntity> getApplicationPeriod() { + final long defaultId = 1L; + + RecruitPeriodResponse applicationPeriod = applyPeriodService.getApplicationPeriod(defaultId); return ResponseEntity .ok() - .body(new CustomResponse( - "success", - 200, - "연세골프 지원 기간 조회 성공", - applyPeriodService.getApplicationPeriod() - )); + .body(CustomResponse.successResponse("연세골프 지원 기간 조회 성공", applicationPeriod)); } @GetMapping("/application/availability") public ResponseEntity> getApplicationAvaliability() { + final long defaultId = 1L; + return ResponseEntity .ok() - .body(new CustomResponse<>( - "success", - 200, + .body(CustomResponse.successResponse( "연세골프 지원 가능 여부 조회 성공", - applyPeriodService.getApplicationAvailability(LocalDate.now()) - )); + applyPeriodService.getApplicationAvailability(LocalDate.now(), defaultId))); } @GetMapping("/admin/forms") - public ResponseEntity>> getApplicationResults(@RequestParam(required = false) Boolean documentPass, - @RequestParam(required = false) Boolean finalPass, - Pageable pageable) { - - System.out.println("documentPass = " + documentPass); - System.out.println("finalPass = " + finalPass); + public ResponseEntity>> getApplicationResults( + @RequestParam(required = false) Boolean documentPass, + @RequestParam(required = false) Boolean finalPass, + Pageable pageable) { return ResponseEntity .ok() - .body(new CustomResponse( - "success", - 200, + .body(CustomResponse.successResponse( "연세골프 지원서 조회 성공", - applicationService.getApplicationResults(documentPass, finalPass, pageable) - )); + applicationService.getApplicationResults(documentPass, finalPass, pageable))); } @GetMapping("/admin/forms/{id}") @@ -111,123 +96,60 @@ public ResponseEntity> getApplication(@PathV return ResponseEntity .ok() - .body(new CustomResponse( - "success", - 200, + .body(CustomResponse.successResponse( "연세골프 지원서 조회 성공", - applicationService.getApplication(id) - )); + applicationService.getApplication(id))); } @PatchMapping("/admin/forms/{id}/documentPass") - public ResponseEntity updateDocumentPass(@PathVariable Long id, @RequestBody DocumentPassRequest documentPass) { + public ResponseEntity> updateDocumentPass(@PathVariable Long id, @RequestBody DocumentPassRequest documentPass) { applicationService.updateDocumentPass(id, documentPass.isDocumentPass()); return ResponseEntity .ok() - .body(new CustomResponse( - "success", - 200, - "연세골프 지원서 서류 합격 수정 성공" - )); + .body(CustomResponse.successResponse("연세골프 지원서 서류 합격 수정 성공")); } @PatchMapping("/admin/forms/{id}/finalPass") - public ResponseEntity updateFinalPass(@PathVariable Long id, @RequestBody FinalPassRequest finalPass) { + public ResponseEntity> updateFinalPass(@PathVariable Long id, @RequestBody FinalPassRequest finalPass) { applicationService.updateFinalPass(id, finalPass.isFinalPass()); return ResponseEntity .ok() - .body(new CustomResponse( - "success", - 200, - "연세골프 지원서 최종 합격 수정 성공" - )); + .body(CustomResponse.successResponse("연세골프 지원서 최종 합격 수정 성공")); } @PatchMapping("/admin/forms/{id}/interviewTime") - public ResponseEntity updateInterviewTime(@PathVariable Long id, @RequestBody UpdateInterviewTimeRequest time) { + public ResponseEntity> updateInterviewTime(@PathVariable Long id, @RequestBody UpdateInterviewTimeRequest time) { applicationService.updateInterviewTime(id, time.getTime()); return ResponseEntity .ok() - .body(new CustomResponse( - "success", - 200, - "연세골프 지원서 면접 시간 수정 성공" - )); - } - - @PostMapping("/admin/forms/documentPassEmail") - public ResponseEntity sendDocumentPassEmail() { - - applicationService.sendDocumentPassEmail(); - - return ResponseEntity - .ok() - .body(new CustomResponse( - "success", - 200, - "연세골프 지원서 서류 합격자 이메일 전송 성공" - )); + .body(CustomResponse.successResponse("연세골프 지원서 면접 시간 수정 성공")); } - @PostMapping("/admin/forms/finalPassEmail") - public ResponseEntity sendFinalPassEmail() { + @PostMapping("/admin/forms/results") + public ResponseEntity> sendEmailNotification(@RequestBody ResultNotification request) { - applicationService.sendFinalPassEmail(); + applicationService.sendEmailNotification(request.isDocumentPass(), request.getFinalPass()); return ResponseEntity .ok() - .body(new CustomResponse( - "success", - 200, - "연세골프 지원서 최종 합격자 이메일 전송 성공" - )); - } - - @PostMapping("/admin/forms/documentFailEmail") - public ResponseEntity sendDocumentFailEmail() { - - applicationService.sendDocumentFailEmail(); - - return ResponseEntity - .ok() - .body(new CustomResponse( - "success", - 200, - "연세골프 지원서 서류 불합격자 이메일 전송 성공" - )); - } - - @PostMapping("/admin/forms/finalFailEmail") - public ResponseEntity sendFinalFailEmail() { - - applicationService.sendFinalFailEmail(); - - return ResponseEntity - .ok() - .body(new CustomResponse( - "success", - 200, - "연세골프 지원서 최종 불합격자 이메일 전송 성공" - )); + .body(CustomResponse.successResponse("연세골프 지원서 결과 이메일 발송 성공")); } @PostMapping("/apply/forms/image") public ResponseEntity> uploadImage(@RequestPart("image") MultipartFile image) { + String imageUrl = imageService.uploadImage(image, RandomString.make(10)); return ResponseEntity - .ok() - .body(new CustomResponse( - "success", - 200, - "연세골프 지원서 이미지 업로드 성공", - imageUrl - )); + .ok() + .body(CustomResponse.successResponse( + "연세골프 지원서 사진 업로드 성공", + new ImageResponse(imageUrl))); } } diff --git a/src/main/java/yonseigolf/server/apply/dto/request/ResultNotification.java b/src/main/java/yonseigolf/server/apply/dto/request/ResultNotification.java new file mode 100644 index 0000000..4a1bdd9 --- /dev/null +++ b/src/main/java/yonseigolf/server/apply/dto/request/ResultNotification.java @@ -0,0 +1,17 @@ +package yonseigolf.server.apply.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ResultNotification { + + private boolean documentPass; + private Boolean finalPass; +} + diff --git a/src/main/java/yonseigolf/server/apply/dto/response/SingleApplicationResult.java b/src/main/java/yonseigolf/server/apply/dto/response/SingleApplicationResult.java index b12039e..ebf57b4 100644 --- a/src/main/java/yonseigolf/server/apply/dto/response/SingleApplicationResult.java +++ b/src/main/java/yonseigolf/server/apply/dto/response/SingleApplicationResult.java @@ -5,7 +5,6 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import yonseigolf.server.apply.entity.Application; import java.time.LocalDateTime; @@ -32,16 +31,4 @@ public SingleApplicationResult(long id, String photo, String name, LocalDateTime this.documentPass = documentPass; this.finalPass = finalPass; } - - public SingleApplicationResult fromApplication(Application Application) { - - return SingleApplicationResult.builder() - .id(Application.getId()) - .photo(Application.getPhoto()) - .name(Application.getName()) - .interviewTime(Application.getInterviewTime()) - .documentPass(Application.getDocumentPass()) - .finalPass(Application.getFinalPass()) - .build(); - } } diff --git a/src/main/java/yonseigolf/server/apply/service/ApplyPeriodService.java b/src/main/java/yonseigolf/server/apply/service/ApplyPeriodService.java index 49297cf..d67ed5e 100644 --- a/src/main/java/yonseigolf/server/apply/service/ApplyPeriodService.java +++ b/src/main/java/yonseigolf/server/apply/service/ApplyPeriodService.java @@ -18,22 +18,30 @@ public ApplyPeriodService(ApplyPeriodRepository repository) { this.repository = repository; } - public RecruitPeriodResponse getApplicationPeriod() { + public RecruitPeriodResponse getApplicationPeriod(long id) { + + RecruitmentPeriod recruitmentPeriod = findById(id); return RecruitPeriodResponse.builder() - .startDate(repository.getOne(1L).getStartDate()) - .endDate(repository.getOne(1L).getEndDate()) - .firstResultDate(repository.getOne(1L).getFirstResultDate()) - .interviewStartDate(repository.getOne(1L).getInterviewStartDate()) - .interviewEndDate(repository.getOne(1L).getInterviewEndDate()) - .finalResultDate(repository.getOne(1L).getFinalResultDate()) - .orientationDate(repository.getOne(1L).getOrientationDate()) + .startDate(recruitmentPeriod.getStartDate()) + .endDate(recruitmentPeriod.getEndDate()) + .firstResultDate(recruitmentPeriod.getFirstResultDate()) + .interviewStartDate(recruitmentPeriod.getInterviewStartDate()) + .interviewEndDate(recruitmentPeriod.getInterviewEndDate()) + .finalResultDate(recruitmentPeriod.getFinalResultDate()) + .orientationDate(recruitmentPeriod.getOrientationDate()) .build(); } - public boolean getApplicationAvailability(LocalDate today) { - RecruitmentPeriod period = repository.getOne(1L); + public boolean getApplicationAvailability(LocalDate today, long periodId) { + RecruitmentPeriod period = findById(periodId); return !today.isBefore(period.getStartDate()) && !today.isAfter(period.getEndDate()); } + + private RecruitmentPeriod findById(long periodId) { + + return repository.findById(periodId).orElseThrow( + () -> new IllegalArgumentException("해당 모집기간이 존재하지 않습니다.")); + } } diff --git a/src/main/java/yonseigolf/server/apply/service/ApplyService.java b/src/main/java/yonseigolf/server/apply/service/ApplyService.java index 5d098af..fd9c211 100644 --- a/src/main/java/yonseigolf/server/apply/service/ApplyService.java +++ b/src/main/java/yonseigolf/server/apply/service/ApplyService.java @@ -13,6 +13,7 @@ import yonseigolf.server.apply.entity.EmailAlarm; import yonseigolf.server.apply.repository.ApplicationRepository; import yonseigolf.server.apply.repository.EmailRepository; +import yonseigolf.server.email.dto.NotificationType; import yonseigolf.server.email.service.EmailService; import java.time.LocalDateTime; @@ -39,7 +40,7 @@ public void apply(ApplicationRequest request) { applicationRepository.save(Application.of(request)); emailService.sendEmail(request.getEmail(), "안녕하세요. 연세골프입니다.\n\n", - request.getName() +"님의 지원서가 정상적으로 제출되었습니다. \n\n" + + request.getName() + "님의 지원서가 정상적으로 제출되었습니다. \n\n" + "서류 합격 여부는 추후 이메일로 공지될 예정입니다. \n\n" + "감사합니다." ); @@ -78,58 +79,27 @@ public void updateInterviewTime(Long id, LocalDateTime time) { findById(id).updateInterviewTime(time); } + public void sendEmailNotification(boolean isDocumentPass, Boolean isFinalPass) { - // TODO: ENUM 활용해서 코드 개선 가능할듯 보인다 - public void sendDocumentPassEmail() { - List applications = findApplicationsByPassFail(true, null); + final NotificationType type = getNotificationType(isDocumentPass, isFinalPass); + final String subject = "안녕하세요. 연세대학교 골프동아리 결과 메일입니다."; - applications.stream() - .forEach(application -> emailService.sendEmail(application.getEmail(), - "안녕하세요. 연세대학교 골프동아리 결과 메일입니다.", - application.getName() +"님 서류 합격 축하드립니다. \n" + - "면접 일정은 추후 공지될 예정입니다. \n" + - "감사합니다.")); + findApplicationsByPassFail(isDocumentPass, isFinalPass) + .forEach(application -> { + final String message = type.generateMessage(application.getName()); + emailService.sendEmail(application.getEmail(), subject, message); + }); } - public void sendFinalPassEmail() { - List applications = findApplicationsByPassFail(true, true); + private NotificationType getNotificationType(boolean isDocumentPass, Boolean isFinalPass) { - applications.stream() - .forEach(application -> emailService.sendEmail(application.getEmail(), - "안녕하세요. 연세대학교 골프동아리 결과 메일입니다.", - application.getName() +"님 최종 합격 축하드립니다. \n" + - "추후 일정은 문자로 공지될 예정입니다. \n" + - "감사합니다.")); - } - - public void sendDocumentFailEmail() { - List applications = findApplicationsByPassFail(false, null); - - applications.stream() - .forEach(application -> emailService.sendEmail(application.getEmail(), - "안녕하세요. 연세대학교 골프동아리 결과 메일입니다.", - application.getName() + "님 연세골프에 지원해주셔서 감사합니다. \n\n\n" + - "안타깝게도 " + application.getName() + "님께 이번 연골 모집에서 합격의 소식을 전해드리지 못하게 되었습니다." + - application.getName() + "님의 뛰어난 열정에도 불구하고, 연세골프는 한정된 인원으로만 운영되는 만큼 아쉽게도 이런 소식을 전해드리게 됐습니다." + - "비록 이번 모집에서 " + application.getName() +"님과 함께하지 못하지만, 다음에 함께 할 수 있기를 바라겠습니다. \n\n" + - "바쁘신 와중에 지원해주셔서 감사합니다. \n\n" + - "연세 골프 운영진 드림") - ); - } - - public void sendFinalFailEmail() { - List applications = findApplicationsByPassFail(true, false); - - applications.stream() - .forEach(application -> emailService.sendEmail(application.getEmail(), - "안녕하세요. 연세대학교 골프동아리 결과 메일입니다.", - application.getName() + "님 연세골프에 지원해주셔서 감사합니다. \n\n\n" + - "안타깝게도 " + application.getName() + "님께 이번 연골 모집에서 합격의 소식을 전해드리지 못하게 되었습니다." + - application.getName() + "님의 뛰어난 열정에도 불구하고, 연세골프는 한정된 인원으로만 운영되는 만큼 아쉽게도 이런 소식을 전해드리게 됐습니다." + - "비록 이번 모집에서 " + application.getName() +"님과 함께하지 못하지만, 다음에 함께 할 수 있기를 바라겠습니다. \n\n" + - "바쁘신 와중에 지원해주셔서 감사합니다. \n\n" + - "연세 골프 운영진 드림") - ); + if (isDocumentPass && isFinalPass == null) { + return NotificationType.DOCUMENT_PASS; + } + if (isDocumentPass && isFinalPass) { + return NotificationType.FINAL_PASS; + } + return NotificationType.FAIL; } private List findApplicationsByPassFail(Boolean docuemntPass, Boolean finalPass) { diff --git a/src/main/java/yonseigolf/server/config/CorsConfig.java b/src/main/java/yonseigolf/server/config/CorsConfig.java index 3b5bf14..5b4ecbb 100644 --- a/src/main/java/yonseigolf/server/config/CorsConfig.java +++ b/src/main/java/yonseigolf/server/config/CorsConfig.java @@ -1,18 +1,28 @@ package yonseigolf.server.config; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpMethod; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +@Profile("!test") @Configuration public class CorsConfig implements Filter { + private static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + private static final String ORIGIN = "Origin"; + private static final String LOCAL_CLIENT = "http://localhost:3000"; + private static final String PROD_CLIENT_WWW = "https://www.yonseigolf.site"; + private static final String PROD_CLIENT = "https://yonseigolf.site"; + private static final String DELIMITER = ", "; + @Override public void init(FilterConfig filterConfig) { - + // 필요한 초기화 작업이 없습니다. } @Override @@ -20,19 +30,26 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; - if (request.getHeader("Origin") != null) { - if (request.getHeader("Origin").contains("yonseigolf.site")) { - response.setHeader("Access-Control-Allow-Origin", "https://www.yonseigolf.site"); + if (request.getHeader(ORIGIN) != null) { + if (request.getHeader(ORIGIN).equals(PROD_CLIENT)) { + response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, PROD_CLIENT); + } else if (request.getHeader(ORIGIN).equals(PROD_CLIENT_WWW)) { + response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, PROD_CLIENT_WWW); } else { - response.setHeader("Access-Control-Allow-Origin", "http://localhost:3000"); + response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, LOCAL_CLIENT); } - } - else { - response.setHeader("Access-Control-Allow-Origin", "http://localhost:3000"); + } else { + response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, LOCAL_CLIENT); } response.setHeader("Access-Control-Allow-Credentials", "true"); - response.setHeader("Access-Control-Allow-Methods","GET, POST, PATCH, DELETE, HEAD, OPTIONS"); + response.setHeader("Access-Control-Allow-Methods", + HttpMethod.GET.name() + DELIMITER + + HttpMethod.POST.name() + DELIMITER + + HttpMethod.PATCH.name() + DELIMITER + + HttpMethod.DELETE.name() + DELIMITER + + HttpMethod.HEAD.name() + DELIMITER + + HttpMethod.OPTIONS.name()); response.setHeader("Access-Control-Max-Age", "10"); response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization"); @@ -42,6 +59,6 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) @Override public void destroy() { - + // 필요한 리소스 정리 작업이 없습니다. } } \ No newline at end of file diff --git a/src/main/java/yonseigolf/server/email/controller/EmailController.java b/src/main/java/yonseigolf/server/email/controller/EmailController.java index b3c3697..0d2a70f 100644 --- a/src/main/java/yonseigolf/server/email/controller/EmailController.java +++ b/src/main/java/yonseigolf/server/email/controller/EmailController.java @@ -18,17 +18,13 @@ public EmailController(EmailService emailService) { this.emailService = emailService; } - @PostMapping("/email/apply-start-email") - public ResponseEntity sendApplyStartAlert() { + @PostMapping("/admin/email/apply-start-email") + public ResponseEntity> sendApplyStartAlert() { emailService.sendApplyStartAlert(); return ResponseEntity .ok() - .body(new CustomResponse( - "success", - 200, - "이메일 전송 성공" - )); + .body(CustomResponse.successResponse("지원 시작 이메일 전송 성공")); } } diff --git a/src/main/java/yonseigolf/server/email/dto/NotificationType.java b/src/main/java/yonseigolf/server/email/dto/NotificationType.java new file mode 100644 index 0000000..a23afb7 --- /dev/null +++ b/src/main/java/yonseigolf/server/email/dto/NotificationType.java @@ -0,0 +1,37 @@ +package yonseigolf.server.email.dto; + +public enum NotificationType { + DOCUMENT_PASS { + @Override + public String generateMessage(String name) { + return name + "님 서류 합격 축하드립니다. \n면접 일정은 추후 공지될 예정입니다. \n감사합니다."; + } + }, + FINAL_PASS { + @Override + public String generateMessage(String name) { + return name + "님 최종 합격 축하드립니다. \n추후 일정은 문자로 공지될 예정입니다. \n감사합니다."; + } + }, + FAIL { + @Override + public String generateMessage(String name) { + return name + "님 연세골프에 지원해주셔서 감사합니다. \n\n\n" + + "안타깝게도 " + name + "님께 이번 연골 모집에서 합격의 소식을 전해드리지 못하게 되었습니다." + + name + "님의 뛰어난 열정에도 불구하고, 연세골프는 한정된 인원으로만 운영되는 만큼 아쉽게도 이런 소식을 전해드리게 됐습니다." + + "비록 이번 모집에서 " + name + "님과 함께하지 못하지만, 다음에 함께 할 수 있기를 바라겠습니다. \n\n" + + "바쁘신 와중에 지원해주셔서 감사합니다. \n\n" + + "연세 골프 운영진 드림"; + } + }, + CLUB_RECRUITMENT{ + @Override + public String generateMessage(String name) { + return "연세대학교 골프동아리입니다. \n" + + "연세대학교 골프동아리 모집이 시작되었습니다.\n " + + "https://yonseigolf.com/apply 에서 확인해주세요"; + } + }; + + public abstract String generateMessage(String name); +} diff --git a/src/main/java/yonseigolf/server/email/service/EmailService.java b/src/main/java/yonseigolf/server/email/service/EmailService.java index 3c9af6f..eb22cf7 100644 --- a/src/main/java/yonseigolf/server/email/service/EmailService.java +++ b/src/main/java/yonseigolf/server/email/service/EmailService.java @@ -7,8 +7,8 @@ import org.springframework.stereotype.Service; import yonseigolf.server.apply.entity.EmailAlarm; import yonseigolf.server.apply.repository.EmailRepository; +import yonseigolf.server.email.dto.NotificationType; -import java.util.ArrayList; import java.util.List; @Slf4j @@ -28,12 +28,9 @@ public EmailService(JavaMailSender mailSender, EmailRepository emailRepository) public void sendApplyStartAlert() { List allAlert = findAllAlert(); - allAlert.stream().forEach(alert -> { - sendEmail(alert.getEmail(), - "연세대학교 골프동아리입니다.", - "연세대학교 골프동아리 모집이 시작되었습니다.\n " + - "https://yonseigolf.com/apply 에서 확인해주세요"); - }); + allAlert.forEach(alert -> sendEmail(alert.getEmail(), + "연세대학교 골프동아리입니다.", + NotificationType.CLUB_RECRUITMENT.generateMessage(null))); emailRepository.deleteAll(); } @@ -53,7 +50,7 @@ public void sendEmail(String to, String subject, String text) { try { mailSender.send(message); } catch (Exception e) { - log.error("이메일 전송 실패 : {}", to , e.getMessage()); + throw new IllegalArgumentException("이메일 전송에 실패했습니다."); } } } diff --git a/src/main/java/yonseigolf/server/user/controller/UserController.java b/src/main/java/yonseigolf/server/user/controller/UserController.java index f7a17f8..afd2a15 100644 --- a/src/main/java/yonseigolf/server/user/controller/UserController.java +++ b/src/main/java/yonseigolf/server/user/controller/UserController.java @@ -28,6 +28,7 @@ @Controller public class UserController { + private static final String SESSION_KAKAO_USER = "kakaoUser"; private final UserService userService; private final OauthLoginService oauthLoginService; private final KakaoOauthInfo kakaoOauthInfo; @@ -41,43 +42,34 @@ public UserController(UserService userService, OauthLoginService oauthLoginServi } @PostMapping("/oauth/kakao") - public ResponseEntity kakaoLogin(@RequestBody KakaoCode kakaoCode, HttpSession session) { + public ResponseEntity> kakaoLogin(@RequestBody KakaoCode kakaoCode, HttpSession session) { - OauthToken oauthToken = oauthLoginService.getOauthToken(kakaoCode.getKakaoCode(), kakaoOauthInfo); + OauthToken oauthToken = oauthLoginService.getOauthToken(kakaoCode.getValue(), kakaoOauthInfo); KakaoLoginResponse kakaoLoginResponse = oauthLoginService.processKakaoLogin(oauthToken.getAccessToken(), kakaoOauthInfo.getLoginUri()); - session.setAttribute("kakaoUser", kakaoLoginResponse.getId()); + session.setAttribute(SESSION_KAKAO_USER, kakaoLoginResponse.getId()); return ResponseEntity .ok() - .body(new CustomResponse( - "success", - 200, - "카카오 로그인 성공" - )); + .body(CustomResponse.successResponse("카카오 로그인 성공")); } @PostMapping("/users/signIn") public ResponseEntity> signIn(HttpSession session) { - Long id = (Long) session.getAttribute("kakaoUser"); + Long id = (Long) session.getAttribute(SESSION_KAKAO_USER); SessionUser sessionUser = userService.signIn(id); session.setAttribute("user", sessionUser); return ResponseEntity .ok() - .body(new CustomResponse<>( - "success", - 200, - "로그인된 유저 정보 조회 성공", - sessionUser - )); + .body(CustomResponse.successResponse("로그인 성공", sessionUser)); } @PostMapping("/users/signUp") - public ResponseEntity signUp(@RequestBody @Validated SignUpUserRequest request, HttpSession session) { + public ResponseEntity> signUp(@RequestBody @Validated SignUpUserRequest request, HttpSession session) { - Long kakaoId = (Long) session.getAttribute("kakaoUser"); + Long kakaoId = (Long) session.getAttribute(SESSION_KAKAO_USER); if (session.getAttribute("user") != null) { throw new IllegalArgumentException("[ERROR] 이미 로그인된 상태입니다."); @@ -88,67 +80,50 @@ public ResponseEntity signUp(@RequestBody @Validated SignUpUserR SessionUser sessionUser = userService.signUp(request, kakaoId); - session.removeAttribute("kakaoUser"); + session.removeAttribute(SESSION_KAKAO_USER); session.setAttribute("user", sessionUser); return ResponseEntity .ok() - .body(new CustomResponse( - "success", - 200, - "회원가입 성공" - )); + .body(CustomResponse.successResponse("회원가입 성공")); } @GetMapping("/admin/users") public ResponseEntity>> findAllUsers(Pageable pageable, UserClass userClass) { + Page users = userService.findUsersByClass(pageable, userClass); + return ResponseEntity .ok() - .body(new CustomResponse<>( - "success", - 200, - "유저 정보 조회 성공", - userService.findAllUsers(pageable, userClass) - )); + .body(CustomResponse.successResponse("유저 조회 성공", users)); } @PatchMapping("/admin/users/{userId}") - public ResponseEntity updateUserClass(@PathVariable Long userId, @RequestBody UserClassRequest userClass) { + public ResponseEntity> updateUserClass(@PathVariable Long userId, @RequestBody UserClassRequest userClass) { userService.updateUserClass(userId, userClass.getUserClass()); return ResponseEntity .ok() - .body(new CustomResponse( - "success", - 200, - "유저 정보 수정 성공" - )); + .body(CustomResponse.successResponse("유저 정보 변경 성공")); } @GetMapping("/users/leaders") public ResponseEntity> getLeaders() { + AdminResponse leaders = userService.getLeaders(); + + return ResponseEntity .ok() - .body(new CustomResponse<>( - "success", - 200, - "로그인된 유저 정보 조회 성공", - userService.getLeaders() - )); + .body(CustomResponse.successResponse("리더 조회 성공", leaders)); } @GetMapping("/healthcheck") - public ResponseEntity healthCheck() { + public ResponseEntity> healthCheck() { return ResponseEntity .ok() - .body(new CustomResponse<>( - "success", - 200, - "헬스체크 성공" - )); + .body(CustomResponse.successResponse("서버 정상 작동 중")); } } diff --git a/src/main/java/yonseigolf/server/user/controller/UserExceptionController.java b/src/main/java/yonseigolf/server/user/controller/UserExceptionController.java index 233acfa..d9e83f4 100644 --- a/src/main/java/yonseigolf/server/user/controller/UserExceptionController.java +++ b/src/main/java/yonseigolf/server/user/controller/UserExceptionController.java @@ -5,20 +5,23 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import yonseigolf.server.util.CustomResponse; +import yonseigolf.server.util.CustomErrorResponse; @Slf4j @RestControllerAdvice public class UserExceptionController { @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity existingMember(IllegalArgumentException ex) { + public ResponseEntity existingMember(IllegalArgumentException ex) { + + log.error("existingMember: {}", ex.getMessage()); return ResponseEntity .status(HttpStatus.UNAUTHORIZED) - .body(new CustomResponse<>( + .body(new CustomErrorResponse( "fail", 401, - ex.getMessage())); + "이미 가입된 회원입니다." + )); } } diff --git a/src/main/java/yonseigolf/server/user/dto/request/KakaoCode.java b/src/main/java/yonseigolf/server/user/dto/request/KakaoCode.java index b59af1e..e7d92f6 100644 --- a/src/main/java/yonseigolf/server/user/dto/request/KakaoCode.java +++ b/src/main/java/yonseigolf/server/user/dto/request/KakaoCode.java @@ -11,5 +11,5 @@ @AllArgsConstructor public class KakaoCode { - String kakaoCode; + String value; } diff --git a/src/main/java/yonseigolf/server/user/dto/response/SessionUser.java b/src/main/java/yonseigolf/server/user/dto/response/SessionUser.java index ed408d5..5f3e898 100644 --- a/src/main/java/yonseigolf/server/user/dto/response/SessionUser.java +++ b/src/main/java/yonseigolf/server/user/dto/response/SessionUser.java @@ -5,9 +5,11 @@ import yonseigolf.server.user.entity.User; import yonseigolf.server.user.entity.UserRole; +import java.io.Serializable; + @Getter @Builder -public class SessionUser { +public class SessionUser implements Serializable { private long id; private String name; diff --git a/src/main/java/yonseigolf/server/user/service/OauthLoginService.java b/src/main/java/yonseigolf/server/user/service/OauthLoginService.java index ccbb577..05e5ca4 100644 --- a/src/main/java/yonseigolf/server/user/service/OauthLoginService.java +++ b/src/main/java/yonseigolf/server/user/service/OauthLoginService.java @@ -19,13 +19,24 @@ public class OauthLoginService { private final RestTemplate restTemplate; @Autowired + public OauthLoginService(RestTemplate restTemplate) { this.restTemplate = restTemplate; } public OauthToken getOauthToken(String code, KakaoOauthInfo oauthInfo) { + HttpEntity request = createRequestEntity(code, oauthInfo); + ResponseEntity response = restTemplate.postForEntity(oauthInfo.getRedirectUri(), request, OauthToken.class); + + return response.getBody(); + } + + public KakaoLoginResponse processKakaoLogin(String accessToken, String loginUri) { + return processLogin(accessToken, loginUri); + } + private HttpEntity createRequestEntity(String code, KakaoOauthInfo oauthInfo) { MultiValueMap headers = new LinkedMultiValueMap<>(); Map header = new HashMap<>(); header.put("Accept", "application/json"); @@ -40,14 +51,10 @@ public OauthToken getOauthToken(String code, KakaoOauthInfo oauthInfo) { requestPayload.put("code", code); requestPayloads.setAll(requestPayload); - HttpEntity request = new HttpEntity<>(requestPayloads, headers); - ResponseEntity response = restTemplate.postForEntity(oauthInfo.getRedirectUri(), request, OauthToken.class); - - return response.getBody(); + return new HttpEntity<>(requestPayloads, headers); } - private T processLogin(String accessToken, String loginUri, Class responseType) { - + private KakaoLoginResponse processLogin(String accessToken, String loginUri) { HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + accessToken); headers.setContentType(MediaType.APPLICATION_JSON); @@ -58,13 +65,7 @@ private T processLogin(String accessToken, String loginUri, Class respon loginUri, HttpMethod.GET, requestEntity, - responseType) + KakaoLoginResponse.class) .getBody(); } - - public KakaoLoginResponse processKakaoLogin(String accessToken, String loginUri) { - - KakaoLoginResponse response = processLogin(accessToken, loginUri, KakaoLoginResponse.class); - return response; - } } diff --git a/src/main/java/yonseigolf/server/user/service/UserService.java b/src/main/java/yonseigolf/server/user/service/UserService.java index d60da46..4ded49d 100644 --- a/src/main/java/yonseigolf/server/user/service/UserService.java +++ b/src/main/java/yonseigolf/server/user/service/UserService.java @@ -53,7 +53,7 @@ public AdminResponse getLeaders() { return AdminResponse.of(UserResponse.fromUser(leader), assistantLeaders); } - public Page findAllUsers(Pageable pageable, UserClass userClass) { + public Page findUsersByClass(Pageable pageable, UserClass userClass) { return repository.findAllUsers(pageable, userClass); } diff --git a/src/main/java/yonseigolf/server/util/AesEncryptor.java b/src/main/java/yonseigolf/server/util/AesEncryptor.java deleted file mode 100644 index b5bc357..0000000 --- a/src/main/java/yonseigolf/server/util/AesEncryptor.java +++ /dev/null @@ -1,82 +0,0 @@ -package yonseigolf.server.util; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import javax.crypto.Cipher; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.StandardCharsets; -import java.security.SecureRandom; -import java.util.Base64; - -@Component -public class AesEncryptor { - - private String ALGORITHM; - private String KEY_STR; - private byte[] KEY; - - @Autowired - public AesEncryptor(@Value("${ALGORITHM}") String algorithm, - @Value("${SECRET_KEY}") String keyStr) { - this.ALGORITHM = algorithm; - this.KEY_STR = keyStr; - this.KEY = KEY_STR.getBytes(StandardCharsets.UTF_8); - } - - public String encrypt(String data) { - - if (data == null) { - return null; - } - - byte[] encryptedBytes = null; - try { - Cipher cipher = Cipher.getInstance(ALGORITHM); - SecretKeySpec secretKey = new SecretKeySpec(KEY, "AES"); - - byte[] iv = new byte[16]; // IV length should be 16 bytes for AES - new SecureRandom().nextBytes(iv); - IvParameterSpec ivSpec = new IvParameterSpec(iv); - - cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); - encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); - - byte[] finalData = new byte[iv.length + encryptedBytes.length]; - System.arraycopy(iv, 0, finalData, 0, iv.length); - System.arraycopy(encryptedBytes, 0, finalData, iv.length, encryptedBytes.length); - - encryptedBytes = finalData; - } catch (Exception e) { - e.printStackTrace(); - } - return Base64.getEncoder().encodeToString(encryptedBytes); - } - - public String decrypt(String encryptedData) { - - if (encryptedData == null) { - return null; - } - - byte[] decryptedBytes = null; - try { - Cipher cipher = Cipher.getInstance(ALGORITHM); - SecretKeySpec secretKey = new SecretKeySpec(KEY, "AES"); - - byte[] decodedData = Base64.getDecoder().decode(encryptedData); - byte[] iv = new byte[16]; // extracting IV from encrypted data - System.arraycopy(decodedData, 0, iv, 0, 16); - - IvParameterSpec ivSpec = new IvParameterSpec(iv); - cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec); - - decryptedBytes = cipher.doFinal(decodedData, 16, decodedData.length - 16); - } catch (Exception e) { - e.printStackTrace(); - } - return new String(decryptedBytes, StandardCharsets.UTF_8); - } -} diff --git a/src/main/java/yonseigolf/server/util/CustomResponse.java b/src/main/java/yonseigolf/server/util/CustomResponse.java index aa860a7..9703bb6 100644 --- a/src/main/java/yonseigolf/server/util/CustomResponse.java +++ b/src/main/java/yonseigolf/server/util/CustomResponse.java @@ -3,11 +3,13 @@ import lombok.AllArgsConstructor; import lombok.Getter; +import org.springframework.http.HttpStatus; @Getter @AllArgsConstructor public class CustomResponse { + private static final String HTTP_STATUS_SUCCESS = "success"; private String status; private int code; private String message; @@ -18,4 +20,23 @@ public CustomResponse(String status, int code, String message) { this.code = code; this.message = message; } + + public static CustomResponse successResponse(String message) { + + return new CustomResponse<>( + HTTP_STATUS_SUCCESS, + HttpStatus.OK.value(), + message + ); + } + + public static CustomResponse successResponse(String message, T data) { + + return new CustomResponse<>( + HTTP_STATUS_SUCCESS, + HttpStatus.OK.value(), + message, + data + ); + } } diff --git a/src/test/java/yonseigolf/server/ServerApplicationTests.java b/src/test/java/yonseigolf/server/ServerApplicationTests.java index 4b6bf93..7c19455 100644 --- a/src/test/java/yonseigolf/server/ServerApplicationTests.java +++ b/src/test/java/yonseigolf/server/ServerApplicationTests.java @@ -6,6 +6,7 @@ @SpringBootTest public class ServerApplicationTests { @Test - void contextLoads() { + void mainTest() { + ServerApplication.main(new String[] {}); } } diff --git a/src/test/java/yonseigolf/server/apply/controller/ApplicationControllerTest.java b/src/test/java/yonseigolf/server/apply/controller/ApplicationControllerTest.java index e723ecc..c2c7b66 100644 --- a/src/test/java/yonseigolf/server/apply/controller/ApplicationControllerTest.java +++ b/src/test/java/yonseigolf/server/apply/controller/ApplicationControllerTest.java @@ -1,6 +1,7 @@ package yonseigolf.server.apply.controller; import com.fasterxml.jackson.core.JsonProcessingException; +import net.bytebuddy.utility.RandomString; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -8,7 +9,9 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.web.multipart.MultipartFile; import yonseigolf.server.apply.dto.request.*; import yonseigolf.server.apply.dto.response.ApplicationResponse; import yonseigolf.server.apply.dto.response.RecruitPeriodResponse; @@ -18,17 +21,20 @@ import yonseigolf.server.apply.service.ApplyService; import yonseigolf.server.docs.utils.RestDocsSupport; +import java.io.IOException; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Arrays; import java.util.List; -import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doNothing; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static yonseigolf.server.docs.utils.ApiDocumentUtils.getDocumentRequest; @@ -171,7 +177,7 @@ void recruitPeriodTest() throws Exception { // when - given(applyPeriodService.getApplicationPeriod()).willReturn(response); + given(applyPeriodService.getApplicationPeriod(anyLong())).willReturn(response); // then mockMvc.perform(get("/application/recruit")) @@ -469,78 +475,66 @@ void updateInterviewTimeTest() throws Exception { } @Test - @DisplayName("서류 합격자들에게 합격 이메일을 전송할 수 있다.") - void sendDocumentPassEmailTest() throws Exception { + @DisplayName("연세 골프 지원 결과를 이메일로 전송할 수 있다.") + void sendResultTest() throws Exception { // given - doNothing().when(applyService).sendDocumentPassEmail(); + ResultNotification request = ResultNotification.builder() + .documentPass(true) + .finalPass(true) + .build(); + doNothing().when(applyService).sendEmailNotification(request.isDocumentPass(), request.getFinalPass()); // when - applyService.sendDocumentPassEmail(); + applyService.sendEmailNotification(request.isDocumentPass(), request.getFinalPass()); // then - mockMvc.perform(post("/admin/forms/documentPassEmail")) - .andExpect(status().isOk()) + mockMvc.perform( + post("/admin/forms/results") + .content(objectMapper.writeValueAsString(request)) + .contentType("application/json") + ) .andDo(print()) - .andDo(document("admin-application-sendDocumentPassEmail-doc", - getDocumentRequest(), - getDocumentResponse() - )); - } - - @Test - @DisplayName("서류 불합격자들에게 불합격 이메일을 전송할 수 있다.") - void sendDocumentFailEmailTest() throws Exception { - // given - doNothing().when(applyService).sendDocumentFailEmail(); - - // when - applyService.sendDocumentFailEmail(); - - // then - mockMvc.perform(post("/admin/forms/documentFailEmail")) .andExpect(status().isOk()) - .andDo(print()) - .andDo(document("admin-application-sendDocumentFailEmail-doc", + .andDo(document("admin-application-sendResult-doc", getDocumentRequest(), - getDocumentResponse() - )); + getDocumentResponse(), + requestFields( + fieldWithPath("documentPass").type(JsonFieldType.BOOLEAN) + .description("서류 합격 여부"), + fieldWithPath("finalPass").type(JsonFieldType.BOOLEAN) + .description("최종 합격 여부") + ))); } @Test - @DisplayName("최종 합격자들에게 합격 이메일을 전송할 수 있다.") - void sendFinalPassEmailTest() throws Exception { + @DisplayName("연세 골프 지원서 사진을 업로드할 수 있다.") + void uploadImageTest() throws Exception { // given - doNothing().when(applyService).sendFinalPassEmail(); - - // when - applyService.sendFinalPassEmail(); - - // then - mockMvc.perform(post("/admin/forms/finalPassEmail")) - .andExpect(status().isOk()) - .andDo(print()) - .andDo(document("admin-application-sendFinalPassEmail-doc", - getDocumentRequest(), - getDocumentResponse() - )); - } + MockMultipartFile image = new MockMultipartFile( + "image", + "image.png", + "image/png", + "image".getBytes()); - @Test - @DisplayName("최종 불합격자들에게 불합격 이메일을 전송할 수 있다.") - void sendFinalFailEmailTest() throws Exception { - // given - doNothing().when(applyService).sendFinalFailEmail(); + given(imageService.uploadImage(any(MultipartFile.class), anyString())).willReturn("url"); // when - applyService.sendFinalFailEmail(); - + imageService.uploadImage(image, RandomString.make(10)); // then - mockMvc.perform(post("/admin/forms/finalPassEmail")) - .andExpect(status().isOk()) + mockMvc.perform( + multipart("/apply/forms/image") + .file(image) + .contentType("multipart/form-data")) .andDo(print()) - .andDo(document("admin-application-sendFinalFailEmail-doc", + .andExpect(status().isOk()) + .andDo(document("admin-application-uploadImage-doc", getDocumentRequest(), - getDocumentResponse() - )); + getDocumentResponse(), + requestParts(partWithName("image").description("업로드할 사진")), + responseFields( + beneathPath("data").withSubsectionId("data"), + fieldWithPath("image").type(JsonFieldType.STRING) + .description("업로드된 사진의 URL") + ))); } } diff --git a/src/test/java/yonseigolf/server/apply/image/ImageServiceTest.java b/src/test/java/yonseigolf/server/apply/image/ImageServiceTest.java new file mode 100644 index 0000000..58c028b --- /dev/null +++ b/src/test/java/yonseigolf/server/apply/image/ImageServiceTest.java @@ -0,0 +1,68 @@ +package yonseigolf.server.apply.image; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@SpringBootTest +class ImageServiceTest { + + @MockBean + private S3Client s3Client; + @Autowired + private ImageService imageService; + + @Test + @DisplayName("사진을 업로드 하면 사진의 url을 반환 받을 수 있다.") + void imageUploadTest() { + // given + MockMultipartFile file = + new MockMultipartFile( + "file", + "test.jpg", + "image/jpeg", + "test image".getBytes()); + + String expectedUrl = "https://yg-img-storage.s3.ap-northeast-2.amazonaws.com/store-image/test.jpgid"; + + given(s3Client.putObject(any(PutObjectRequest.class), any(RequestBody.class))).willReturn(null); + + // when + String returnUrl = imageService.uploadImage(file, "id"); + + // then + assertThat(expectedUrl).isEqualTo(returnUrl); + } + + @Test + @DisplayName("파일에 문제가 있을 시 IllegalStateException을 반환한다.") + void fileUploadThrowTest() throws IOException { + // given + MultipartFile file = mock(MultipartFile.class); + given(file.getOriginalFilename()).willReturn("test.jpg"); + given(file.getContentType()).willReturn("image/jpeg"); + given(file.getSize()).willReturn(10L); + given(file.getInputStream()).willThrow(IOException.class); // This will throw IOException + + // when & then + assertThatThrownBy(() -> imageService.uploadImage(file, "id")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Failed to upload file"); + } +} diff --git a/src/test/java/yonseigolf/server/apply/service/ApplicationPeriodServiceTest.java b/src/test/java/yonseigolf/server/apply/service/ApplicationPeriodServiceTest.java new file mode 100644 index 0000000..47004da --- /dev/null +++ b/src/test/java/yonseigolf/server/apply/service/ApplicationPeriodServiceTest.java @@ -0,0 +1,136 @@ +package yonseigolf.server.apply.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import yonseigolf.server.apply.dto.response.RecruitPeriodResponse; +import yonseigolf.server.apply.entity.RecruitmentPeriod; +import yonseigolf.server.apply.repository.ApplyPeriodRepository; + +import javax.transaction.Transactional; +import java.time.LocalDate; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@Transactional +@SpringBootTest +class ApplicationPeriodServiceTest { + + @Autowired + private ApplyPeriodRepository applyPeriodRepository; + @Autowired + private ApplyPeriodService applyPeriodService; + + @Test + @DisplayName("지원 기간을 조회할 수 있다.") + void applyPeriodTest() { + // given + RecruitPeriodResponse result = RecruitPeriodResponse.builder() + .startDate(LocalDate.of(2021, 8, 1)) + .endDate(LocalDate.of(2021, 8, 1)) + .firstResultDate(LocalDate.of(2021, 8, 1)) + .interviewStartDate(LocalDate.of(2021, 8, 1)) + .interviewEndDate(LocalDate.of(2021, 8, 1)) + .finalResultDate(LocalDate.of(2021, 8, 1)) + .orientationDate(LocalDate.of(2021, 8, 1)) + .build(); + + RecruitmentPeriod recruitmentPeriod = RecruitmentPeriod.builder() + .startDate(LocalDate.of(2021, 8, 1)) + .endDate(LocalDate.of(2021, 8, 1)) + .firstResultDate(LocalDate.of(2021, 8, 1)) + .interviewStartDate(LocalDate.of(2021, 8, 1)) + .interviewEndDate(LocalDate.of(2021, 8, 1)) + .finalResultDate(LocalDate.of(2021, 8, 1)) + .orientationDate(LocalDate.of(2021, 8, 1)) + .build(); + + RecruitmentPeriod saved = applyPeriodRepository.save(recruitmentPeriod); + + // when + RecruitPeriodResponse applicationPeriod = applyPeriodService.getApplicationPeriod(saved.getId()); + // then + assertAll( + () -> assertThat(applicationPeriod.getStartDate()).isEqualTo(result.getStartDate()), + () -> assertThat(applicationPeriod.getEndDate()).isEqualTo(result.getEndDate()), + () -> assertThat(applicationPeriod.getFirstResultDate()).isEqualTo(result.getFirstResultDate()), + () -> assertThat(applicationPeriod.getInterviewStartDate()).isEqualTo(result.getInterviewStartDate()), + () -> assertThat(applicationPeriod.getInterviewEndDate()).isEqualTo(result.getInterviewEndDate()), + () -> assertThat(applicationPeriod.getFinalResultDate()).isEqualTo(result.getFinalResultDate()), + () -> assertThat(applicationPeriod.getOrientationDate()).isEqualTo(result.getOrientationDate()) + ); + } + + @Test + @DisplayName("오늘이 지원 기간 이전이라면 false를 반환") + void getApplicationAvailabilityTest() { + // given + RecruitmentPeriod recruitmentPeriod = RecruitmentPeriod.builder() + .startDate(LocalDate.of(2021, 8, 1)) + .endDate(LocalDate.of(2021, 8, 1)) + .firstResultDate(LocalDate.of(2021, 8, 1)) + .interviewStartDate(LocalDate.of(2021, 8, 1)) + .interviewEndDate(LocalDate.of(2021, 8, 1)) + .finalResultDate(LocalDate.of(2021, 8, 1)) + .orientationDate(LocalDate.of(2021, 8, 1)) + .build(); + RecruitmentPeriod saved = applyPeriodRepository.save(recruitmentPeriod); + + // when + boolean applicationAvailability = applyPeriodService.getApplicationAvailability(LocalDate.now(), saved.getId()); + + // then + assertThat(applicationAvailability).isFalse(); + } + + @Test + @DisplayName("오늘이 지원 기간 이전이라면 false를 반환") + void getApplicationAvailabilityFalseTest() { + // given + RecruitmentPeriod recruitmentPeriod = RecruitmentPeriod.builder() + .startDate(LocalDate.of(2024, 8, 1)) + .endDate(LocalDate.of(2024, 8, 1)) + .firstResultDate(LocalDate.of(2021, 8, 1)) + .interviewStartDate(LocalDate.of(2021, 8, 1)) + .interviewEndDate(LocalDate.of(2021, 8, 1)) + .finalResultDate(LocalDate.of(2021, 8, 1)) + .orientationDate(LocalDate.of(2021, 8, 1)) + .build(); + RecruitmentPeriod saved = applyPeriodRepository.save(recruitmentPeriod); + + // when + boolean applicationAvailability = applyPeriodService.getApplicationAvailability(LocalDate.now(), saved.getId()); + + // then + assertThat(applicationAvailability).isFalse(); + } + + @Test + @DisplayName("오늘이 지원 기간이라면 true를 반환") + void getApplicationAvailabilityTrueTest() { + // given + LocalDate now = LocalDate.now(); + LocalDate endDate = now.plusDays(1); + + + RecruitmentPeriod recruitmentPeriod = RecruitmentPeriod.builder() + .startDate(now) + .endDate(endDate) + .firstResultDate(LocalDate.of(2021, 8, 1)) + .interviewStartDate(LocalDate.of(2021, 8, 1)) + .interviewEndDate(LocalDate.of(2021, 8, 1)) + .finalResultDate(LocalDate.of(2021, 8, 1)) + .orientationDate(LocalDate.of(2021, 8, 1)) + .build(); + + RecruitmentPeriod saved = applyPeriodRepository.save(recruitmentPeriod); + + // when + boolean applicationAvailability = applyPeriodService.getApplicationAvailability(now, saved.getId()); + + // then + assertThat(applicationAvailability).isTrue(); + } +} diff --git a/src/test/java/yonseigolf/server/apply/service/ApplyServiceTest.java b/src/test/java/yonseigolf/server/apply/service/ApplyServiceTest.java new file mode 100644 index 0000000..5d66a28 --- /dev/null +++ b/src/test/java/yonseigolf/server/apply/service/ApplyServiceTest.java @@ -0,0 +1,240 @@ +package yonseigolf.server.apply.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import yonseigolf.server.apply.dto.request.ApplicationRequest; +import yonseigolf.server.apply.dto.request.EmailAlertRequest; +import yonseigolf.server.apply.dto.response.ApplicationResponse; +import yonseigolf.server.apply.dto.response.SingleApplicationResult; +import yonseigolf.server.apply.entity.Application; +import yonseigolf.server.apply.entity.EmailAlarm; +import yonseigolf.server.apply.repository.ApplicationRepository; +import yonseigolf.server.apply.repository.EmailRepository; +import yonseigolf.server.email.service.EmailService; + + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@SpringBootTest +class ApplyServiceTest { + + @Autowired + private ApplyService applyService; + @Autowired + private ApplicationRepository applicationRepository; + @Autowired + private EmailRepository emailRepository; + @MockBean + private EmailService emailService; + + @BeforeEach + void setUp() { + + applicationRepository.deleteAll(); + } + + @Test + @DisplayName("지원서를 제출할 수 있다.") + void applyTest() { + // given + ApplicationRequest request = ApplicationRequest.builder() + .email("email") + .build(); + doNothing().when(emailService).sendEmail(any(), any(), any()); + + // when + applyService.apply(request); + + // then + assertAll( + () -> assertThat(applicationRepository.findAll()).hasSize(1), + () -> verify(emailService, times(1)).sendEmail(any(), any(), any()) + ); + } + + @Test + @DisplayName("이메일 알림 신청 테스트") + void emailAlarmTest() { + // given + EmailAlertRequest request = EmailAlertRequest.builder() + .email("email") + .build(); + + EmailAlarm saved = emailRepository.save(EmailAlarm.of(request)); + + // when + applyService.emailAlarm(request); + + // then + assertThat(emailRepository.findById(saved.getId()).get().getEmail()).isEqualTo("email"); + } + + @ParameterizedTest + @DisplayName("지원서 조회 테스트") + @ValueSource(strings = {"true", "false", "null"}) + void getApplicationResultsTest(String documentPassStr) { + // given + Boolean documentPass = null; + + if(!documentPassStr.equals("null")) { + documentPass = Boolean.parseBoolean(documentPassStr); + } + + Boolean finalPass = true; + Application application = Application.builder() + .documentPass(documentPass) + .finalPass(finalPass) + .build(); + + applicationRepository.save(application); + + // when + Page resultPage = applyService.getApplicationResults(documentPass, finalPass, PageRequest.of(0, 10)); + + // then + assertThat(resultPage.getTotalElements()).isEqualTo(1); + } + + @Test + @DisplayName("지원서를 조회할 수 있다.") + void getApplicationTest() { + // given + Application application = Application.builder() + .age(20) + .photo("photo") + .name("name") + .phoneNumber("phoneNumber") + .email("email") + .build(); + Application saved = applicationRepository.save(application); + + // when + ApplicationResponse response = applyService.getApplication(saved.getId()); + + // then + assertAll( + () -> assertThat(response.getAge()).isEqualTo(application.getAge()), + () -> assertThat(response.getPhoto()).isEqualTo(application.getPhoto()), + () -> assertThat(response.getName()).isEqualTo(application.getName()), + () -> assertThat(response.getPhoneNumber()).isEqualTo(application.getPhoneNumber()), + () -> assertThat(response.getEmail()).isEqualTo(application.getEmail()) + ); + } + + @ParameterizedTest + @DisplayName("서류 결과를 업데이트 할 수 있다.") + @CsvSource({"true,true", "false,false"}) + void updateDocumentPassTest(boolean documentPass, boolean expected) { + // given + Application application = Application.builder() + .documentPass(!documentPass) + .build(); + Application saved = applicationRepository.save(application); + + // when + applyService.updateDocumentPass(saved.getId(), documentPass); + Application result = applicationRepository.findById(saved.getId()).get(); + + // then + assertThat(result.getDocumentPass()).isEqualTo(expected); + } + + @ParameterizedTest + @DisplayName("최종 결과를 업데이트 할 수 있다.") + @CsvSource({"true,true", "false,false"}) + void updateFinalPassTest(boolean finalPass, boolean expected) { + // given + Application application = Application.builder() + .finalPass(!finalPass) + .build(); + Application saved = applicationRepository.save(application); + + // when + applyService.updateFinalPass(saved.getId(), finalPass); + Application result = applicationRepository.findById(saved.getId()).get(); + + // then + assertThat(result.getFinalPass()).isEqualTo(expected); + } + + @Test + @DisplayName("면접 시간을 업데이트할 수 있다.") + void interviewUpdateTest() { + // given + Application application = Application.builder() + .interviewTime(null) + .build(); + Application saved = applicationRepository.save(application); + LocalDateTime updateInterviewTime = LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS); + + // when + applyService.updateInterviewTime(saved.getId(), updateInterviewTime); + Application updated = applicationRepository.findById(saved.getId()).get(); + + // then + assertThat(updated.getInterviewTime()).isEqualTo(updateInterviewTime); + } + + @Test + @DisplayName("지원서가 존재하지 않는데 인터뷰 시간을 업데이트 하려고 하면 예외가 발생한다.") + void test() { + // given + Application application = Application.builder().build(); + applicationRepository.save(application); + Long notExistId = 100L; + + // when & then + assertThatThrownBy(() -> applyService.updateInterviewTime(notExistId, LocalDateTime.now())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("해당 지원서가 존재하지 않습니다."); + + } + + @ParameterizedTest + @DisplayName("서류 및 최종 합격 결과에 따른 이메일 알림을 전송 한다.") + @CsvSource({"true,null", "true,true", "true,false", "false,false"}) + void sendEmailNotificationTest(boolean isDocumentPass, String finalPass) { + // given + Boolean isFinalPass = null; + if (!finalPass.equals("null")) { + isFinalPass = Boolean.parseBoolean(finalPass); + } + Application application = Application.builder() + .name("name") + .email("email") + .documentPass(isDocumentPass) + .finalPass(isFinalPass) + .build(); + + System.out.println("isDocumentPass = " + isDocumentPass); + System.out.println("isFinalPass = " + isFinalPass); + System.out.println((isDocumentPass && isFinalPass == null)); + + + applicationRepository.save(application); + int expected = applicationRepository.findAll().size(); + + // when + applyService.sendEmailNotification(isDocumentPass, isFinalPass); + + // then + verify(emailService, times(expected)).sendEmail(any(), any(), any()); + } +} + diff --git a/src/test/java/yonseigolf/server/config/CorsConfigTest.java b/src/test/java/yonseigolf/server/config/CorsConfigTest.java new file mode 100644 index 0000000..52fb3ff --- /dev/null +++ b/src/test/java/yonseigolf/server/config/CorsConfigTest.java @@ -0,0 +1,63 @@ +package yonseigolf.server.config; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +public class CorsConfigTest { + + private CorsConfig corsConfig; + private MockHttpServletRequest request; + private MockHttpServletResponse response; + private FilterChain chain; + + @BeforeEach + public void setUp() { + corsConfig = new CorsConfig(); + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + chain = mock(FilterChain.class); + } + + @Test + public void whenOriginIsYonseiGolf_thenSetAllowOriginToYonseiGolfSite() throws IOException, ServletException { + request.addHeader("Origin", "https://yonseigolf.site"); + + corsConfig.doFilter(request, response, chain); + + assertEquals("https://yonseigolf.site", response.getHeader("Access-Control-Allow-Origin")); + } + + @Test + public void whenOriginIsWWWYonseiGolf_thenSetAllowOriginToYonseiGolfSite() throws IOException, ServletException { + request.addHeader("Origin", "https://www.yonseigolf.site"); + + corsConfig.doFilter(request, response, chain); + + assertEquals("https://www.yonseigolf.site", response.getHeader("Access-Control-Allow-Origin")); + } + + @Test + public void whenOriginIsNotYonseiGolf_thenSetAllowOriginToLocalhost() throws IOException, ServletException { + request.addHeader("Origin", "https://other.site"); + + corsConfig.doFilter(request, response, chain); + + assertEquals("http://localhost:3000", response.getHeader("Access-Control-Allow-Origin")); + } + + @Test + public void whenNoOriginIsSet_thenDefaultToAllowOriginLocalhost() throws IOException, ServletException { + corsConfig.doFilter(request, response, chain); + + assertEquals("http://localhost:3000", response.getHeader("Access-Control-Allow-Origin")); + } +} diff --git a/src/test/java/yonseigolf/server/email/controller/EmailControllerTest.java b/src/test/java/yonseigolf/server/email/controller/EmailControllerTest.java new file mode 100644 index 0000000..0c8621e --- /dev/null +++ b/src/test/java/yonseigolf/server/email/controller/EmailControllerTest.java @@ -0,0 +1,49 @@ +package yonseigolf.server.email.controller; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import yonseigolf.server.docs.utils.RestDocsSupport; +import yonseigolf.server.email.service.EmailService; + +import static org.mockito.Mockito.doNothing; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static yonseigolf.server.docs.utils.ApiDocumentUtils.getDocumentRequest; +import static yonseigolf.server.docs.utils.ApiDocumentUtils.getDocumentResponse; + +@ExtendWith(MockitoExtension.class) +public class EmailControllerTest extends RestDocsSupport { + + @Mock + private EmailService emailService; + + @Test + @DisplayName("지원 시작 이메일을 보낸다.") + void applyStartMailTest() throws Exception { + // given + doNothing().when(emailService).sendApplyStartAlert(); + + // when + emailService.sendApplyStartAlert(); + + // then + mockMvc.perform( + post("/admin/email/apply-start-email") + ).andDo(print()) + .andExpect(status().isOk()) + .andDo(document("email-apply-start-email", + getDocumentRequest(), + getDocumentResponse())); + } + + + @Override + protected Object initController() { + return new EmailController(emailService); + } +} diff --git a/src/test/java/yonseigolf/server/email/dto/NotificationTypeTest.java b/src/test/java/yonseigolf/server/email/dto/NotificationTypeTest.java new file mode 100644 index 0000000..125a355 --- /dev/null +++ b/src/test/java/yonseigolf/server/email/dto/NotificationTypeTest.java @@ -0,0 +1,49 @@ +package yonseigolf.server.email.dto; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class NotificationTypeTest { + + @Test + void testDocumentPassMessage() { + // given + String name = "John"; + String expectedMessage = "John님 서류 합격 축하드립니다. \n면접 일정은 추후 공지될 예정입니다. \n감사합니다."; + + // when + String result = NotificationType.DOCUMENT_PASS.generateMessage(name); + + assertThat(result).isEqualTo(expectedMessage); + } + + @Test + void testFinalPassMessage() { + // given + String name = "John"; + String expectedMessage = "John님 최종 합격 축하드립니다. \n추후 일정은 문자로 공지될 예정입니다. \n감사합니다."; + + // when + String result = NotificationType.FINAL_PASS.generateMessage(name); + + assertThat(result).isEqualTo(expectedMessage); + } + + @Test + void testFailMessage() { + // given + String name = "John"; + String expectedMessage = "John님 연세골프에 지원해주셔서 감사합니다. \n\n\n" + + "안타깝게도 John님께 이번 연골 모집에서 합격의 소식을 전해드리지 못하게 되었습니다." + + "John님의 뛰어난 열정에도 불구하고, 연세골프는 한정된 인원으로만 운영되는 만큼 아쉽게도 이런 소식을 전해드리게 됐습니다." + + "비록 이번 모집에서 John님과 함께하지 못하지만, 다음에 함께 할 수 있기를 바라겠습니다. \n\n" + + "바쁘신 와중에 지원해주셔서 감사합니다. \n\n" + + "연세 골프 운영진 드림"; + + // when + String result = NotificationType.FAIL.generateMessage(name); + + assertThat(result).isEqualTo(expectedMessage); + } +} diff --git a/src/test/java/yonseigolf/server/email/service/EmailServiceTest.java b/src/test/java/yonseigolf/server/email/service/EmailServiceTest.java new file mode 100644 index 0000000..9f4e729 --- /dev/null +++ b/src/test/java/yonseigolf/server/email/service/EmailServiceTest.java @@ -0,0 +1,72 @@ +package yonseigolf.server.email.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import yonseigolf.server.apply.entity.EmailAlarm; +import yonseigolf.server.apply.repository.EmailRepository; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@SpringBootTest +public class EmailServiceTest { + + @Mock + private JavaMailSender mailSender; + + @Mock + private EmailRepository emailRepository; + + @InjectMocks + private EmailService emailService; + + @Test + @DisplayName("지원 시작 이메일을 보낸다.") + public void sendApplyStartAlertTest() { + // Given + List alerts = Arrays.asList( + EmailAlarm.builder() + .id(1L) + .email("email") + .build() + ); + given(emailRepository.findAll()).willReturn(alerts); + + // When + emailService.sendApplyStartAlert(); + + // Then + assertAll( + () -> verify(emailRepository, times(1)).findAll(), + () -> verify(mailSender, times(alerts.size())).send(any(SimpleMailMessage.class)), + () -> verify(emailRepository, times(1)).deleteAll() + ); + } + + @Test + @DisplayName("이메일 전송 시 예외가 발생했을 경우, catch 후 illeagalArgumentException을 발생시킨다.") + void testSendEmail_ExceptionThrown() { + // given + String to = "test@example.com"; + String subject = "Test Subject"; + String text = "Test Text"; + + doThrow(new IllegalArgumentException()).when(mailSender).send(any(SimpleMailMessage.class)); + + // then + assertThrows(IllegalArgumentException.class, () -> { + emailService.sendEmail(to, subject, text); + }); + } +} diff --git a/src/test/java/yonseigolf/server/user/controller/UserControllerTest.java b/src/test/java/yonseigolf/server/user/controller/UserControllerTest.java index ebab9e8..c20c088 100644 --- a/src/test/java/yonseigolf/server/user/controller/UserControllerTest.java +++ b/src/test/java/yonseigolf/server/user/controller/UserControllerTest.java @@ -25,8 +25,8 @@ import java.util.List; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doNothing; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; @@ -54,7 +54,7 @@ void kakaoOauthLoginTest() throws Exception { MockHttpSession session = new MockHttpSession(); KakaoCode kakaoCode = KakaoCode.builder() - .kakaoCode("kakaoCode") + .value("kakaoCode") .build(); OauthToken oauthToken = OauthToken.builder() @@ -68,7 +68,7 @@ void kakaoOauthLoginTest() throws Exception { .id(1L) .build(); - given(oauthLoginService.getOauthToken(kakaoCode.getKakaoCode(), kakaoOauthInfo)).willReturn(oauthToken); + given(oauthLoginService.getOauthToken(kakaoCode.getValue(), kakaoOauthInfo)).willReturn(oauthToken); given(oauthLoginService.processKakaoLogin(oauthToken.getAccessToken(), kakaoOauthInfo.getLoginUri())).willReturn(kakaoLoginResponse); // when @@ -78,7 +78,7 @@ void kakaoOauthLoginTest() throws Exception { mockMvc.perform( post("/oauth/kakao") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(kakaoCode.getKakaoCode())) + .content(objectMapper.writeValueAsString(kakaoCode.getValue())) .session(session) ).andDo(print()) .andExpect(status().isOk()) @@ -162,6 +162,32 @@ void signUpTest() throws Exception { )); } + @Test + @DisplayName("로그인된 상태라면 에러를 발생한다.") + void loggedInErrorTest() throws Exception { + // given + MockHttpSession session = new MockHttpSession(); + session.setAttribute("user", SessionUser.builder().id(1L).build()); + + // when & then + mockMvc.perform(post("/users/signUp").session(session)) + .andExpect(status().is4xxClientError()) + .andExpect(result -> assertFalse(result.getResolvedException() instanceof IllegalArgumentException)); + } + + @Test + @DisplayName("카카오 로그인을 하지 않고 회원가입을 할 경우 에러를 발생한다.") + void kakaoLoginErrorTest() throws Exception { + // given + MockHttpSession session = new MockHttpSession(); + + // when & then + mockMvc.perform(post("/users/signUp").session(session)) + .andExpect(status().is4xxClientError()) + .andExpect(result -> assertFalse(result.getResolvedException() instanceof IllegalArgumentException)); + } + + @Test @DisplayName("회장 및 부회장 정보 조회 테스트") void getLeadersTest() throws Exception { @@ -225,11 +251,11 @@ void findAllUserTest() throws Exception { ); Page mockPage = new PageImpl<>(users); - given(userService.findAllUsers(any(), any())).willReturn(mockPage); - given(userService.findAllUsers(any(), any())).willReturn(mockPage); + given(userService.findUsersByClass(any(), any())).willReturn(mockPage); + given(userService.findUsersByClass(any(), any())).willReturn(mockPage); // when - userService.findAllUsers(any(), any()); + userService.findUsersByClass(any(), any()); // then mockMvc.perform(get("/admin/users") @@ -309,6 +335,18 @@ void updateUserClassTest() throws Exception { )); } + @Test + @DisplayName("Health check test") + void healthCheck() throws Exception { + + mockMvc.perform(get("/healthcheck")) + .andExpect(status().isOk()) + .andDo(print()) + .andDo(document("healthcheck-doc", + getDocumentRequest(), + getDocumentResponse())); + } + @Override protected Object initController() { return new UserController(userService, oauthLoginService, kakaoOauthInfo); diff --git a/src/test/java/yonseigolf/server/user/controller/UserIntegrateControllerTest.java b/src/test/java/yonseigolf/server/user/controller/UserIntegrateControllerTest.java new file mode 100644 index 0000000..67c811c --- /dev/null +++ b/src/test/java/yonseigolf/server/user/controller/UserIntegrateControllerTest.java @@ -0,0 +1,86 @@ +package yonseigolf.server.user.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +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.context.SpringBootTest; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import yonseigolf.server.user.dto.request.SignUpUserRequest; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +public class UserIntegrateControllerTest { + + private MockMvc mockMvc; + + @Autowired + private UserController userController; // UserController 주입 + + @Autowired + private UserExceptionController userExceptionController; // UserExceptionController 주입 + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders + .standaloneSetup(userController) // standalone 설정 + .setControllerAdvice(userExceptionController) // UserExceptionController 추가 + .build(); + } + + @Test + @DisplayName("이미 로그인한 경우 401 에러가 발생한다.") + public void testAlreadyLoggedIn() throws Exception { + // given + MockHttpSession session = new MockHttpSession(); + session.setAttribute("user", "testUser"); + + SignUpUserRequest request = SignUpUserRequest.builder() + .name("testName") + .phoneNumber("010-1234-5678") + .studentId(12) + .major("testMajor") + .semester(1) + .build(); + + String requestBody = new ObjectMapper().writeValueAsString(request); + + // when & then + mockMvc.perform(post("/users/signUp") + .content(requestBody) + .contentType("application/json") + .session(session)) + .andExpect(status().isUnauthorized()); + } + + @Test + @DisplayName("로그인 하지 않았지만, 카카오 로그인을 하지 않은 경우 401 에러가 발생한다.") + void test() throws Exception { + // given + MockHttpSession session = new MockHttpSession(); + + SignUpUserRequest request = SignUpUserRequest.builder() + .name("testName") + .phoneNumber("010-1234-5678") + .studentId(12) + .major("testMajor") + .semester(1) + .build(); + + String requestBody = new ObjectMapper().writeValueAsString(request); + + // when & then + mockMvc.perform(post("/users/signUp") + .content(requestBody) + .contentType("application/json") + .session(session)) + .andExpect(status().isUnauthorized()); + } +} \ No newline at end of file diff --git a/src/test/java/yonseigolf/server/user/service/OauthLoginServiceTest.java b/src/test/java/yonseigolf/server/user/service/OauthLoginServiceTest.java new file mode 100644 index 0000000..9dfc87f --- /dev/null +++ b/src/test/java/yonseigolf/server/user/service/OauthLoginServiceTest.java @@ -0,0 +1,84 @@ +package yonseigolf.server.user.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; +import yonseigolf.server.user.dto.response.KakaoAccount; +import yonseigolf.server.user.dto.response.KakaoLoginResponse; +import yonseigolf.server.user.dto.token.KakaoOauthInfo; +import yonseigolf.server.user.dto.token.OauthToken; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +@SpringBootTest +class OauthLoginServiceTest { + + @Autowired + private OauthLoginService oauthLoginService; + + @MockBean + private RestTemplate restTemplate; + + @Test + @DisplayName("카카오 토큰을 받아올 수 있다.") + public void testGetOauthToken() { + // given + String code = "sampleCode"; + String sampleRedirectUri = "http://sampleRedirectUri.com"; + KakaoOauthInfo oauthInfo = KakaoOauthInfo.builder().redirectUri(sampleRedirectUri).build(); + OauthToken expectedToken = OauthToken.builder().build(); + ResponseEntity mockedResponse = new ResponseEntity<>(expectedToken, HttpStatus.OK); + + given(restTemplate.postForEntity(anyString(), any(), eq(OauthToken.class))).willReturn(mockedResponse); + + // when + OauthToken resultToken = oauthLoginService.getOauthToken(code, oauthInfo); + + // then + verify(restTemplate, times(1)).postForEntity(anyString(), any(), eq(OauthToken.class)); + assertEquals(expectedToken, resultToken); + } + + @Test + @DisplayName("카카오 로그인을 진행할 수 있다.") + void kakaoLoginTest() { + // given + String accessToken = "sampleAccessToken"; + String loginUri = "sampleLoginUri"; + KakaoLoginResponse expectedResponse = KakaoLoginResponse.builder() + .id(1L) + .kakaoAccount( + KakaoAccount + .builder() + .email("email") + .build() + ) + .build(); + ResponseEntity mockResponseEntity = new ResponseEntity<>(expectedResponse, HttpStatus.OK); + + given(restTemplate.exchange( + eq(loginUri), + eq(HttpMethod.GET), + any(HttpEntity.class), + eq(KakaoLoginResponse.class))) + .willReturn(mockResponseEntity); + + // when + KakaoLoginResponse actualResponse = oauthLoginService.processKakaoLogin(accessToken, loginUri); + + + // then + assertThat(actualResponse).isEqualTo(expectedResponse); + } +} diff --git a/src/test/java/yonseigolf/server/user/service/UserServiceTest.java b/src/test/java/yonseigolf/server/user/service/UserServiceTest.java new file mode 100644 index 0000000..2cd179c --- /dev/null +++ b/src/test/java/yonseigolf/server/user/service/UserServiceTest.java @@ -0,0 +1,196 @@ +package yonseigolf.server.user.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import yonseigolf.server.user.dto.request.SignUpUserRequest; +import yonseigolf.server.user.dto.response.AdminResponse; +import yonseigolf.server.user.dto.response.SessionUser; +import yonseigolf.server.user.dto.response.SingleUserResponse; +import yonseigolf.server.user.entity.User; +import yonseigolf.server.user.entity.UserClass; +import yonseigolf.server.user.entity.UserRole; +import yonseigolf.server.user.repository.UserRepository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +@Transactional +@SpringBootTest +@ActiveProfiles("test") +class UserServiceTest { + + @Autowired + private UserRepository userRepository; + @Autowired + private UserService userService; + + @Test + @DisplayName("사용자는 회원가입을 할 수 있다.") + void signUpTest() { + // given + SignUpUserRequest request = SignUpUserRequest.builder() + .name("이름") + .phoneNumber("010-1234-5678") + .studentId(1) + .major("컴퓨터과학과") + .semester(1) + .build(); + Long kaKaoId = 1L; + + // when + SessionUser sessionUser = userService.signUp(request, kaKaoId); + long userId = sessionUser.getId(); + User user = userRepository.findById(userId).get(); + + // then + assertAll( + () -> assertThat(user.getName()).isEqualTo(request.getName()), + () -> assertThat(user.getPhoneNumber()).isEqualTo(request.getPhoneNumber()), + () -> assertThat(user.getStudentId()).isEqualTo(request.getStudentId()), + () -> assertThat(user.getMajor()).isEqualTo(request.getMajor()), + () -> assertThat(user.getSemester()).isEqualTo(request.getSemester()), + () -> assertThat(user.getKakaoId()).isEqualTo(kaKaoId), + () -> assertThat(user.getRole()).isEqualTo(UserRole.MEMBER), + () -> assertThat(user.getUserClass()).isEqualTo(UserClass.NONE), + () -> assertThat(sessionUser.isAdminStatus()).isFalse() + ); + } + + @ParameterizedTest + @DisplayName("회원 가입이 되어 있다면 kakaoId로 로그인을 할 수 있다.") + @EnumSource(UserRole.class) + void signInTest(UserRole userRole) { + // given + + User user = createUser(UserClass.OB, userRole); + + User save = userRepository.save(user); + + // when + SessionUser sessionUser = userService.signIn(save.getKakaoId()); + + // then + assertAll( + () -> assertThat(sessionUser.getId()).isEqualTo(save.getId()), + () -> assertThat(sessionUser.getName()).isEqualTo(save.getName()) + ); + } + + @Test + @DisplayName("회장 1명과 부회장 2명을 조회할 수 있다.") + void getLeadersTest() { + // given + User leader = createUser(UserClass.OB, UserRole.LEADER); + User firstAssistantLeader = createUser(UserClass.OB, UserRole.ASSISTANT_LEADER); + User secondAssistantLeader = createUser(UserClass.OB, UserRole.ASSISTANT_LEADER); + + userRepository.save(leader); + userRepository.save(firstAssistantLeader); + userRepository.save(secondAssistantLeader); + + // when + AdminResponse leaders = userService.getLeaders(); + + // then + assertAll( + () -> assertThat(leaders.getLeader().getName()).isEqualTo(leader.getName()), + () -> assertThat(leaders.getAssistantLeaders().get(0).getName()).isEqualTo(firstAssistantLeader.getName()), + () -> assertThat(leaders.getAssistantLeaders().get(1).getName()).isEqualTo(secondAssistantLeader.getName()) + ); + } + + @Test + @DisplayName("사용자는 OB, YB 별로 회원을 조회할 수 있다.") + void findUsersByClassTest() { + // given + User user = createUser(UserClass.OB, UserRole.MEMBER); + User secondUser = createUser(UserClass.OB, UserRole.MEMBER); + + userRepository.save(user); + userRepository.save(secondUser); + + // when + Page allUsersByClass = userService.findUsersByClass(PageRequest.of(0, Integer.MAX_VALUE), UserClass.OB); + + // then + assertAll( + () -> assertThat(allUsersByClass.getTotalElements()).isEqualTo(2), + () -> assertThat(allUsersByClass.getContent().get(0).getUserClass()).isEqualTo(UserClass.OB), + () -> assertThat(allUsersByClass.getContent().get(1).getUserClass()).isEqualTo(UserClass.OB) + ); + } + + @Test + @DisplayName("회원의 OB, YB 상태를 변경할 수 있다.") + void updateUserClassTest() { + // given + User user = createUser(UserClass.OB, UserRole.MEMBER); + User savedUser = userRepository.save(user); + UserClass updatedUserClass = UserClass.YB; + + // when + userService.updateUserClass(savedUser.getId(), updatedUserClass); + User updatedUser = userRepository.findById(savedUser.getId()).get(); + + // then + assertThat(updatedUser.getUserClass()).isEqualTo(updatedUserClass); + } + + private User createUser(UserClass userClass, UserRole userRole) { + + return User.builder() + .name("이름") + .phoneNumber("010-1234-5678") + .studentId(1) + .major("컴퓨터과학과") + .semester(1) + .kakaoId(1L) + .role(userRole) + .userClass(userClass) + .build(); + } + + @Test + @DisplayName("로그인을 하는데 카카오 아이디가 없다면 예외가 발생한다.") + void noKakaoIdExceptionTest() { + // given + Long notExistKakaoId = 100L; + + // when & then + assertThatThrownBy(() -> userService.signIn(notExistKakaoId)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("존재하지 않는 유저입니다."); + } + + @Test + @DisplayName("회장을 지정해주지 않은 경우 예외가 발생한다.") + void noLeaderException() { + + // when & then + assertThatThrownBy(() -> userService.getLeaders()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("회장이 존재하지 않습니다."); + } + + @Test + @DisplayName("없는 유저에게 메서드로 유저 클래스를 변경하려고 하면 예외가 발생한다.") + void notExistingUserExceptionTest() { + // given + Long notExistingUserId = 100L; + + // then + assertThatThrownBy(() -> userService.updateUserClass(notExistingUserId, UserClass.OB)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("존재하지 않는 유저입니다."); + + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000..81c59a1 --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,10 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb;MODE=MySQL;DB_CLOSE_DELAY=-1 + username: sa + password: + driver-class-name: org.h2.Driver + jpa: + properties: + hibernate: + ddl-auto: create-drop