From 22e6ad4f71cc5097de37444f37b6461b9758df19 Mon Sep 17 00:00:00 2001 From: pricelees Date: Sun, 13 Jul 2025 12:18:50 +0000 Subject: [PATCH] =?UTF-8?q?[#3]=20=EC=9D=B8=EC=A6=9D=20/=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20=EC=BD=94=EB=93=9C=20=EC=BD=94=ED=8B=80=EB=A6=B0=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=20(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 📝 관련 이슈 및 PR **PR과 관련된 이슈 번호** - #3 ## ✨ 작업 내용 ### 0. 공통 - 패키지 구조 수정(web, business, infrastructure 구조) - Swagger-UI 어노테이션을 별도의 인터페이스를 만들어 컨트롤러에서 분리 - 결합도 높은 클래스 통합(ex: Member 엔티티와 Role enum 등) ### 1. 회원 도메인 - 기능 자체가 적어서 변화된 내용이 크게 없음. 패키지 구조 수정과 클래스 통합 정도의 과정이 대부분이었음. ### 2. 인증 도메인 - 전체적으로 코드 중복이 많아, 확장함수 및 클래스 통합으로 중복 코드를 상당히 많이 제거하였음. - JwtHandler와 Interceptor에서 모두 이뤄지던 null 예외 처리를 JwtHandler에서만 처리하도록 수정 ## 🧪 테스트 - 모든 테스트는 Kotest 기반으로 수정 & 로그인 및 토큰 처리가 필요한 API 테스트는 mocking을 활용하도록 수정 - 향후 테스트도 꼭 필요하다고 느껴지는 테스트가 아니라면 DB 사용보다는 mocking을 활용할 예정 ## 📚 참고 자료 및 기타 Reviewed-on: https://gitea.pricelees.me/pricelees/roomescape-refactored/pulls/4 Co-authored-by: pricelees Co-committed-by: pricelees --- .../member/business/MemberService.kt | 43 ++++++ .../member/controller/MemberController.java | 37 ----- .../java/roomescape/member/domain/Member.java | 98 ------------ .../java/roomescape/member/domain/Role.java | 6 - .../domain/repository/MemberRepository.java | 12 -- .../roomescape/member/dto/MemberResponse.java | 14 -- .../member/dto/MembersResponse.java | 11 -- .../infrastructure/persistence/Member.kt | 23 +++ .../persistence/MemberRepository.kt | 7 + .../member/service/MemberService.java | 47 ------ .../java/roomescape/member/web/MemberAPI.kt | 21 +++ .../roomescape/member/web/MemberController.kt | 48 ++++++ .../controller/ReservationController.java | 6 +- .../controller/ReservationTimeController.java | 4 +- .../reservation/domain/Reservation.java | 2 +- .../dto/response/ReservationResponse.java | 2 +- .../service/ReservationService.java | 8 +- .../system/auth/annotation/Admin.java | 11 -- .../system/auth/annotation/LoginRequired.java | 11 -- .../system/auth/annotation/MemberId.java | 11 -- .../auth/controller/AuthController.java | 102 ------------- .../system/auth/dto/LoginCheckResponse.java | 9 -- .../system/auth/dto/LoginRequest.java | 17 --- .../auth/infrastructure/jwt/JwtHandler.kt | 50 +++++++ .../auth/interceptor/AdminInterceptor.java | 86 ----------- .../auth/interceptor/LoginInterceptor.java | 79 ---------- .../system/auth/jwt/JwtHandler.java | 65 -------- .../system/auth/jwt/dto/TokenDto.java | 4 - .../auth/resolver/MemberIdResolver.java | 53 ------- .../system/auth/service/AuthService.java | 34 ----- .../system/auth/service/AuthService.kt | 32 ++++ .../roomescape/system/auth/web/AuthAPI.kt | 67 +++++++++ .../system/auth/web/AuthController.kt | 54 +++++++ .../roomescape/system/auth/web/AuthDTO.kt | 30 ++++ .../auth/web/support/AuthAnnotations.kt | 13 ++ .../auth/web/support/AuthInterceptors.kt | 90 ++++++++++++ .../system/auth/web/support/CookieUtils.kt | 27 ++++ .../auth/web/support/MemberIdResolver.kt | 33 +++++ .../system/config/WebMvcConfig.java | 6 +- .../system/exception/ErrorType.java | 1 + .../theme/controller/ThemeController.java | 4 +- .../view/controller/PageController.kt | 4 +- src/test/java/roomescape/common/Fixtures.kt | 25 +++- .../roomescape/common/RoomescapeApiTest.kt | 93 ++++++++++++ .../global/auth/jwt/JwtHandlerTest.java | 118 --------------- .../controller/MemberControllerTest.java | 72 --------- .../member/controller/MemberControllerTest.kt | 66 +++++++++ .../roomescape/member/domain/MemberTest.java | 26 ---- .../payment/domain/PaymentTest.java | 6 +- .../payment/service/PaymentServiceTest.java | 12 +- .../controller/ReservationControllerTest.java | 50 ++++--- .../ReservationTimeControllerTest.java | 10 +- .../reservation/domain/ReservationTest.java | 10 +- .../ReservationSearchSpecificationTest.java | 8 +- .../service/ReservationServiceTest.java | 32 ++-- .../service/ReservationTimeServiceTest.java | 8 +- .../ReservationWithPaymentServiceTest.java | 14 +- .../system/auth/business/AuthServiceTest.kt | 83 +++++++++++ .../auth/controller/AuthControllerTest.java | 118 --------------- .../auth/infrastructure/jwt/JwtHandlerTest.kt | 61 ++++++++ .../system/auth/service/AuthServiceTest.java | 65 -------- .../system/auth/web/AuthControllerTest.kt | 139 ++++++++++++++++++ .../theme/controller/ThemeControllerTest.java | 8 +- .../theme/service/ThemeServiceTest.java | 10 +- .../view/controller/PageControllerTest.kt | 103 +++---------- 65 files changed, 1127 insertions(+), 1292 deletions(-) create mode 100644 src/main/java/roomescape/member/business/MemberService.kt delete mode 100644 src/main/java/roomescape/member/controller/MemberController.java delete mode 100644 src/main/java/roomescape/member/domain/Member.java delete mode 100644 src/main/java/roomescape/member/domain/Role.java delete mode 100644 src/main/java/roomescape/member/domain/repository/MemberRepository.java delete mode 100644 src/main/java/roomescape/member/dto/MemberResponse.java delete mode 100644 src/main/java/roomescape/member/dto/MembersResponse.java create mode 100644 src/main/java/roomescape/member/infrastructure/persistence/Member.kt create mode 100644 src/main/java/roomescape/member/infrastructure/persistence/MemberRepository.kt delete mode 100644 src/main/java/roomescape/member/service/MemberService.java create mode 100644 src/main/java/roomescape/member/web/MemberAPI.kt create mode 100644 src/main/java/roomescape/member/web/MemberController.kt delete mode 100644 src/main/java/roomescape/system/auth/annotation/Admin.java delete mode 100644 src/main/java/roomescape/system/auth/annotation/LoginRequired.java delete mode 100644 src/main/java/roomescape/system/auth/annotation/MemberId.java delete mode 100644 src/main/java/roomescape/system/auth/controller/AuthController.java delete mode 100644 src/main/java/roomescape/system/auth/dto/LoginCheckResponse.java delete mode 100644 src/main/java/roomescape/system/auth/dto/LoginRequest.java create mode 100644 src/main/java/roomescape/system/auth/infrastructure/jwt/JwtHandler.kt delete mode 100644 src/main/java/roomescape/system/auth/interceptor/AdminInterceptor.java delete mode 100644 src/main/java/roomescape/system/auth/interceptor/LoginInterceptor.java delete mode 100644 src/main/java/roomescape/system/auth/jwt/JwtHandler.java delete mode 100644 src/main/java/roomescape/system/auth/jwt/dto/TokenDto.java delete mode 100644 src/main/java/roomescape/system/auth/resolver/MemberIdResolver.java delete mode 100644 src/main/java/roomescape/system/auth/service/AuthService.java create mode 100644 src/main/java/roomescape/system/auth/service/AuthService.kt create mode 100644 src/main/java/roomescape/system/auth/web/AuthAPI.kt create mode 100644 src/main/java/roomescape/system/auth/web/AuthController.kt create mode 100644 src/main/java/roomescape/system/auth/web/AuthDTO.kt create mode 100644 src/main/java/roomescape/system/auth/web/support/AuthAnnotations.kt create mode 100644 src/main/java/roomescape/system/auth/web/support/AuthInterceptors.kt create mode 100644 src/main/java/roomescape/system/auth/web/support/CookieUtils.kt create mode 100644 src/main/java/roomescape/system/auth/web/support/MemberIdResolver.kt create mode 100644 src/test/java/roomescape/common/RoomescapeApiTest.kt delete mode 100644 src/test/java/roomescape/global/auth/jwt/JwtHandlerTest.java delete mode 100644 src/test/java/roomescape/member/controller/MemberControllerTest.java create mode 100644 src/test/java/roomescape/member/controller/MemberControllerTest.kt delete mode 100644 src/test/java/roomescape/member/domain/MemberTest.java create mode 100644 src/test/java/roomescape/system/auth/business/AuthServiceTest.kt delete mode 100644 src/test/java/roomescape/system/auth/controller/AuthControllerTest.java create mode 100644 src/test/java/roomescape/system/auth/infrastructure/jwt/JwtHandlerTest.kt delete mode 100644 src/test/java/roomescape/system/auth/service/AuthServiceTest.java create mode 100644 src/test/java/roomescape/system/auth/web/AuthControllerTest.kt diff --git a/src/main/java/roomescape/member/business/MemberService.kt b/src/main/java/roomescape/member/business/MemberService.kt new file mode 100644 index 00000000..f52269f4 --- /dev/null +++ b/src/main/java/roomescape/member/business/MemberService.kt @@ -0,0 +1,43 @@ +package roomescape.member.business + +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import roomescape.member.infrastructure.persistence.Member +import roomescape.member.infrastructure.persistence.MemberRepository +import roomescape.member.web.MembersResponse +import roomescape.member.web.toResponse +import roomescape.system.exception.ErrorType +import roomescape.system.exception.RoomEscapeException + +@Service +@Transactional(readOnly = true) +class MemberService( + private val memberRepository: MemberRepository +) { + fun readAllMembers(): MembersResponse = MembersResponse( + memberRepository.findAll() + .map { it.toResponse() } + .toList() + ) + + fun findById(memberId: Long): Member = memberRepository.findByIdOrNull(memberId) + ?: throw RoomEscapeException( + ErrorType.MEMBER_NOT_FOUND, + String.format("[memberId: %d]", memberId), + HttpStatus.BAD_REQUEST + ) + + fun findMemberByEmailAndPassword(email: String, password: String): Member = + memberRepository.findByEmailAndPassword(email, password) + ?: throw RoomEscapeException( + ErrorType.MEMBER_NOT_FOUND, + String.format("[email: %s, password: %s]", email, password), + HttpStatus.BAD_REQUEST + ) + + fun existsById(memberId: Long): Boolean = memberRepository.existsById(memberId) + +} + diff --git a/src/main/java/roomescape/member/controller/MemberController.java b/src/main/java/roomescape/member/controller/MemberController.java deleted file mode 100644 index 96cf2728..00000000 --- a/src/main/java/roomescape/member/controller/MemberController.java +++ /dev/null @@ -1,37 +0,0 @@ -package roomescape.member.controller; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import roomescape.member.dto.MembersResponse; -import roomescape.member.service.MemberService; -import roomescape.system.auth.annotation.Admin; -import roomescape.system.dto.response.RoomEscapeApiResponse; - -@RestController -@Tag(name = "2. 회원 API", description = "회원 정보를 관리할 때 사용합니다.") -public class MemberController { - - private final MemberService memberService; - - public MemberController(MemberService memberService) { - this.memberService = memberService; - } - - @Admin - @GetMapping("/members") - @ResponseStatus(HttpStatus.OK) - @Operation(summary = "모든 회원 조회", tags = "관리자 로그인이 필요한 API") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true) - }) - public RoomEscapeApiResponse getAllMembers() { - return RoomEscapeApiResponse.success(memberService.findAllMembers()); - } -} diff --git a/src/main/java/roomescape/member/domain/Member.java b/src/main/java/roomescape/member/domain/Member.java deleted file mode 100644 index 5654a7da..00000000 --- a/src/main/java/roomescape/member/domain/Member.java +++ /dev/null @@ -1,98 +0,0 @@ -package roomescape.member.domain; - -import org.springframework.http.HttpStatus; - -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import roomescape.system.exception.ErrorType; -import roomescape.system.exception.RoomEscapeException; - -@Entity -public class Member { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - private String name; - - private String email; - - private String password; - - @Enumerated(value = EnumType.STRING) - private Role role; - - protected Member() { - } - - public Member( - String name, - String email, - String password, - Role role - ) { - this(null, name, email, password, role); - } - - public Member( - Long id, - String name, - String email, - String password, - Role role - ) { - this.id = id; - this.name = name; - this.email = email; - this.password = password; - this.role = role; - - validateRole(); - } - - private void validateRole() { - if (role == null) { - throw new RoomEscapeException(ErrorType.REQUEST_DATA_BLANK, String.format("[values: %s]", this), - HttpStatus.BAD_REQUEST); - } - } - - public boolean isAdmin() { - return this.role == Role.ADMIN; - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public String getEmail() { - return email; - } - - public String getPassword() { - return password; - } - - public Role getRole() { - return role; - } - - @Override - public String toString() { - return "Member{" + - "id=" + id + - ", name=" + name + - ", email=" + email + - ", password=" + password + - ", role=" + role + - '}'; - } -} diff --git a/src/main/java/roomescape/member/domain/Role.java b/src/main/java/roomescape/member/domain/Role.java deleted file mode 100644 index e573fc02..00000000 --- a/src/main/java/roomescape/member/domain/Role.java +++ /dev/null @@ -1,6 +0,0 @@ -package roomescape.member.domain; - -public enum Role { - MEMBER, - ADMIN -} diff --git a/src/main/java/roomescape/member/domain/repository/MemberRepository.java b/src/main/java/roomescape/member/domain/repository/MemberRepository.java deleted file mode 100644 index 4f039582..00000000 --- a/src/main/java/roomescape/member/domain/repository/MemberRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package roomescape.member.domain.repository; - -import java.util.Optional; - -import org.springframework.data.jpa.repository.JpaRepository; - -import roomescape.member.domain.Member; - -public interface MemberRepository extends JpaRepository { - - Optional findByEmailAndPassword(String email, String password); -} diff --git a/src/main/java/roomescape/member/dto/MemberResponse.java b/src/main/java/roomescape/member/dto/MemberResponse.java deleted file mode 100644 index 8683b6aa..00000000 --- a/src/main/java/roomescape/member/dto/MemberResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package roomescape.member.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import roomescape.member.domain.Member; - -@Schema(name = "회원 조회 응답", description = "회원 정보 조회 응답시 사용됩니다.") -public record MemberResponse( - @Schema(description = "회원 번호. 회원을 식별할 때 사용합니다.") Long id, - @Schema(description = "회원의 이름") String name -) { - public static MemberResponse fromEntity(Member member) { - return new MemberResponse(member.getId(), member.getName()); - } -} diff --git a/src/main/java/roomescape/member/dto/MembersResponse.java b/src/main/java/roomescape/member/dto/MembersResponse.java deleted file mode 100644 index 5a83c97d..00000000 --- a/src/main/java/roomescape/member/dto/MembersResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package roomescape.member.dto; - -import java.util.List; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(name = "회원 목록 조회 응답", description = "모든 회원의 정보 조회 응답시 사용됩니다.") -public record MembersResponse( - @Schema(description = "모든 회원의 ID 및 이름") List members -) { -} diff --git a/src/main/java/roomescape/member/infrastructure/persistence/Member.kt b/src/main/java/roomescape/member/infrastructure/persistence/Member.kt new file mode 100644 index 00000000..1e63e863 --- /dev/null +++ b/src/main/java/roomescape/member/infrastructure/persistence/Member.kt @@ -0,0 +1,23 @@ +package roomescape.member.infrastructure.persistence + +import jakarta.persistence.* + +@Entity +class Member( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + var name: String, + var email: String, + var password: String, + + @Enumerated(value = EnumType.STRING) + var role: Role +) { + fun isAdmin(): Boolean = role == Role.ADMIN +} + +enum class Role { + MEMBER, + ADMIN, +} diff --git a/src/main/java/roomescape/member/infrastructure/persistence/MemberRepository.kt b/src/main/java/roomescape/member/infrastructure/persistence/MemberRepository.kt new file mode 100644 index 00000000..c08e3920 --- /dev/null +++ b/src/main/java/roomescape/member/infrastructure/persistence/MemberRepository.kt @@ -0,0 +1,7 @@ +package roomescape.member.infrastructure.persistence + +import org.springframework.data.jpa.repository.JpaRepository + +interface MemberRepository : JpaRepository { + fun findByEmailAndPassword(email: String, password: String): Member? +} diff --git a/src/main/java/roomescape/member/service/MemberService.java b/src/main/java/roomescape/member/service/MemberService.java deleted file mode 100644 index ff5dbd4c..00000000 --- a/src/main/java/roomescape/member/service/MemberService.java +++ /dev/null @@ -1,47 +0,0 @@ -package roomescape.member.service; - -import java.util.List; - -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import roomescape.member.domain.Member; -import roomescape.member.domain.repository.MemberRepository; -import roomescape.member.dto.MemberResponse; -import roomescape.member.dto.MembersResponse; -import roomescape.system.exception.ErrorType; -import roomescape.system.exception.RoomEscapeException; - -@Service -public class MemberService { - - private final MemberRepository memberRepository; - - public MemberService(MemberRepository memberRepository) { - this.memberRepository = memberRepository; - } - - @Transactional(readOnly = true) - public MembersResponse findAllMembers() { - List response = memberRepository.findAll().stream() - .map(MemberResponse::fromEntity) - .toList(); - - return new MembersResponse(response); - } - - @Transactional(readOnly = true) - public Member findMemberById(Long memberId) { - return memberRepository.findById(memberId) - .orElseThrow(() -> new RoomEscapeException(ErrorType.MEMBER_NOT_FOUND, - String.format("[memberId: %d]", memberId), HttpStatus.BAD_REQUEST)); - } - - @Transactional(readOnly = true) - public Member findMemberByEmailAndPassword(String email, String password) { - return memberRepository.findByEmailAndPassword(email, password) - .orElseThrow(() -> new RoomEscapeException(ErrorType.MEMBER_NOT_FOUND, - String.format("[email: %s, password: %s]", email, password), HttpStatus.BAD_REQUEST)); - } -} diff --git a/src/main/java/roomescape/member/web/MemberAPI.kt b/src/main/java/roomescape/member/web/MemberAPI.kt new file mode 100644 index 00000000..92aa4bbd --- /dev/null +++ b/src/main/java/roomescape/member/web/MemberAPI.kt @@ -0,0 +1,21 @@ +package roomescape.member.web + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.ResponseStatus +import roomescape.system.auth.web.support.Admin +import roomescape.system.dto.response.RoomEscapeApiResponse + +@Tag(name = "2. 회원 API", description = "회원 정보를 관리할 때 사용합니다.") +interface MemberAPI { + + @Admin + @Operation(summary = "모든 회원 조회", tags = ["관리자 로그인이 필요한 API"]) + @ApiResponses(ApiResponse(responseCode = "200", description = "성공", useReturnTypeSchema = true)) + @ResponseStatus(HttpStatus.OK) + fun readAllMembers(): RoomEscapeApiResponse + +} diff --git a/src/main/java/roomescape/member/web/MemberController.kt b/src/main/java/roomescape/member/web/MemberController.kt new file mode 100644 index 00000000..1889ebb3 --- /dev/null +++ b/src/main/java/roomescape/member/web/MemberController.kt @@ -0,0 +1,48 @@ +package roomescape.member.web + +import io.swagger.v3.oas.annotations.media.Schema +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController +import roomescape.member.business.MemberService +import roomescape.member.infrastructure.persistence.Member +import roomescape.system.dto.response.RoomEscapeApiResponse + +@RestController +class MemberController( + private val memberService: MemberService +) : MemberAPI { + + @GetMapping("/members") + override fun readAllMembers(): RoomEscapeApiResponse { + val result: MembersResponse = memberService.readAllMembers() + + return RoomEscapeApiResponse.success(result) + } +} + +@Schema(name = "회원 조회 응답", description = "회원 정보 조회 응답시 사용됩니다.") +data class MemberResponse( + @field:Schema(description = "회원의 고유 번호") + val id: Long, + + @field:Schema(description = "회원의 이름") + val name: String +) { + companion object { + @JvmStatic + fun fromEntity(member: Member): MemberResponse { + return MemberResponse(member.id!!, member.name) + } + } +} + +fun Member.toResponse(): MemberResponse = MemberResponse( + id = id!!, + name = name +) + +@Schema(name = "회원 목록 조회 응답", description = "모든 회원의 정보 조회 응답시 사용됩니다.") +data class MembersResponse( + @field:Schema(description = "모든 회원의 ID 및 이름") + val members: List +) diff --git a/src/main/java/roomescape/reservation/controller/ReservationController.java b/src/main/java/roomescape/reservation/controller/ReservationController.java index 2836c12d..0b2c2705 100644 --- a/src/main/java/roomescape/reservation/controller/ReservationController.java +++ b/src/main/java/roomescape/reservation/controller/ReservationController.java @@ -37,9 +37,9 @@ import roomescape.reservation.dto.response.ReservationResponse; import roomescape.reservation.dto.response.ReservationsResponse; import roomescape.reservation.service.ReservationService; import roomescape.reservation.service.ReservationWithPaymentService; -import roomescape.system.auth.annotation.Admin; -import roomescape.system.auth.annotation.LoginRequired; -import roomescape.system.auth.annotation.MemberId; +import roomescape.system.auth.web.support.Admin; +import roomescape.system.auth.web.support.LoginRequired; +import roomescape.system.auth.web.support.MemberId; import roomescape.system.dto.response.ErrorResponse; import roomescape.system.dto.response.RoomEscapeApiResponse; import roomescape.system.exception.RoomEscapeException; diff --git a/src/main/java/roomescape/reservation/controller/ReservationTimeController.java b/src/main/java/roomescape/reservation/controller/ReservationTimeController.java index bd6d3fd8..6d804b1a 100644 --- a/src/main/java/roomescape/reservation/controller/ReservationTimeController.java +++ b/src/main/java/roomescape/reservation/controller/ReservationTimeController.java @@ -28,8 +28,8 @@ import roomescape.reservation.dto.response.ReservationTimeInfosResponse; import roomescape.reservation.dto.response.ReservationTimeResponse; import roomescape.reservation.dto.response.ReservationTimesResponse; import roomescape.reservation.service.ReservationTimeService; -import roomescape.system.auth.annotation.Admin; -import roomescape.system.auth.annotation.LoginRequired; +import roomescape.system.auth.web.support.Admin; +import roomescape.system.auth.web.support.LoginRequired; import roomescape.system.dto.response.ErrorResponse; import roomescape.system.dto.response.RoomEscapeApiResponse; diff --git a/src/main/java/roomescape/reservation/domain/Reservation.java b/src/main/java/roomescape/reservation/domain/Reservation.java index 8fee6fc6..7d300176 100644 --- a/src/main/java/roomescape/reservation/domain/Reservation.java +++ b/src/main/java/roomescape/reservation/domain/Reservation.java @@ -15,7 +15,7 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import roomescape.member.domain.Member; +import roomescape.member.infrastructure.persistence.Member; import roomescape.system.exception.ErrorType; import roomescape.system.exception.RoomEscapeException; import roomescape.theme.domain.Theme; diff --git a/src/main/java/roomescape/reservation/dto/response/ReservationResponse.java b/src/main/java/roomescape/reservation/dto/response/ReservationResponse.java index c4d5c8fa..ba4ecb2b 100644 --- a/src/main/java/roomescape/reservation/dto/response/ReservationResponse.java +++ b/src/main/java/roomescape/reservation/dto/response/ReservationResponse.java @@ -5,7 +5,7 @@ import java.time.LocalDate; import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; -import roomescape.member.dto.MemberResponse; +import roomescape.member.web.MemberResponse; import roomescape.reservation.domain.Reservation; import roomescape.reservation.domain.ReservationStatus; import roomescape.theme.dto.ThemeResponse; diff --git a/src/main/java/roomescape/reservation/service/ReservationService.java b/src/main/java/roomescape/reservation/service/ReservationService.java index 05f22a41..20a856c6 100644 --- a/src/main/java/roomescape/reservation/service/ReservationService.java +++ b/src/main/java/roomescape/reservation/service/ReservationService.java @@ -9,8 +9,8 @@ import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import roomescape.member.domain.Member; -import roomescape.member.service.MemberService; +import roomescape.member.business.MemberService; +import roomescape.member.infrastructure.persistence.Member; import roomescape.reservation.domain.Reservation; import roomescape.reservation.domain.ReservationStatus; import roomescape.reservation.domain.ReservationTime; @@ -147,7 +147,7 @@ public class ReservationService { ReservationStatus status) { ReservationTime time = reservationTimeService.findTimeById(timeId); Theme theme = themeService.findThemeById(themeId); - Member member = memberService.findMemberById(memberId); + Member member = memberService.findById(memberId); validateDateAndTime(date, time); return new Reservation(date, time, theme, member, status); @@ -213,7 +213,7 @@ public class ReservationService { } private void validateIsMemberAdmin(Long memberId) { - Member member = memberService.findMemberById(memberId); + Member member = memberService.findById(memberId); if (member.isAdmin()) { return; } diff --git a/src/main/java/roomescape/system/auth/annotation/Admin.java b/src/main/java/roomescape/system/auth/annotation/Admin.java deleted file mode 100644 index e525ecc6..00000000 --- a/src/main/java/roomescape/system/auth/annotation/Admin.java +++ /dev/null @@ -1,11 +0,0 @@ -package roomescape.system.auth.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface Admin { -} diff --git a/src/main/java/roomescape/system/auth/annotation/LoginRequired.java b/src/main/java/roomescape/system/auth/annotation/LoginRequired.java deleted file mode 100644 index e2df7c1f..00000000 --- a/src/main/java/roomescape/system/auth/annotation/LoginRequired.java +++ /dev/null @@ -1,11 +0,0 @@ -package roomescape.system.auth.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface LoginRequired { -} diff --git a/src/main/java/roomescape/system/auth/annotation/MemberId.java b/src/main/java/roomescape/system/auth/annotation/MemberId.java deleted file mode 100644 index 208b6ee2..00000000 --- a/src/main/java/roomescape/system/auth/annotation/MemberId.java +++ /dev/null @@ -1,11 +0,0 @@ -package roomescape.system.auth.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target(ElementType.PARAMETER) -@Retention(RetentionPolicy.RUNTIME) -public @interface MemberId { -} diff --git a/src/main/java/roomescape/system/auth/controller/AuthController.java b/src/main/java/roomescape/system/auth/controller/AuthController.java deleted file mode 100644 index 4d2d87e5..00000000 --- a/src/main/java/roomescape/system/auth/controller/AuthController.java +++ /dev/null @@ -1,102 +0,0 @@ -package roomescape.system.auth.controller; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.validation.Valid; -import roomescape.system.auth.annotation.LoginRequired; -import roomescape.system.auth.annotation.MemberId; -import roomescape.system.auth.dto.LoginCheckResponse; -import roomescape.system.auth.dto.LoginRequest; -import roomescape.system.auth.jwt.dto.TokenDto; -import roomescape.system.auth.service.AuthService; -import roomescape.system.dto.response.ErrorResponse; -import roomescape.system.dto.response.RoomEscapeApiResponse; - -@RestController -@Tag(name = "1. 인증 / 인가 API", description = "로그인, 로그아웃 및 로그인 상태를 확인합니다") -public class AuthController { - - private final AuthService authService; - - public AuthController(AuthService authService) { - this.authService = authService; - } - - @PostMapping("/login") - @ResponseStatus(HttpStatus.OK) - @Operation(summary = "로그인") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "로그인 성공시 쿠키에 토큰 정보를 저장합니다."), - @ApiResponse(responseCode = "400", description = "존재하지 않는 회원이거나, 이메일 또는 비밀번호가 잘못 입력되었습니다.", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))) - }) - public RoomEscapeApiResponse login( - @Valid @RequestBody LoginRequest loginRequest, - HttpServletResponse response - ) { - TokenDto tokenInfo = authService.login(loginRequest); - addCookieToResponse(new Cookie("accessToken", tokenInfo.accessToken()), response); - return RoomEscapeApiResponse.success(); - } - - @GetMapping("/login/check") - @ResponseStatus(HttpStatus.OK) - @Operation(summary = "로그인 상태 확인") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "로그인 상태이며, 로그인된 회원의 이름을 반환합니다."), - @ApiResponse(responseCode = "400", description = "쿠키에 있는 토큰 정보로 회원을 조회할 수 없습니다.", - content = @Content(schema = @Schema(implementation = ErrorResponse.class))), - }) - public RoomEscapeApiResponse checkLogin(@MemberId @Parameter(hidden = true) Long memberId) { - LoginCheckResponse response = authService.checkLogin(memberId); - return RoomEscapeApiResponse.success(response); - } - - @LoginRequired - @PostMapping("/logout") - @ResponseStatus(HttpStatus.OK) - @Operation(summary = "로그아웃", tags = "로그인이 필요한 API") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "로그아웃 성공시 쿠키에 저장된 토큰 정보를 삭제합니다.") - }) - public RoomEscapeApiResponse logout( - HttpServletRequest request, - HttpServletResponse response - ) { - Cookie cookie = getTokenCookie(request); - cookie.setValue(null); - cookie.setMaxAge(0); - addCookieToResponse(cookie, response); - return RoomEscapeApiResponse.success(); - } - - private Cookie getTokenCookie(HttpServletRequest request) { - for (Cookie cookie : request.getCookies()) { - if (cookie.getName().equals("accessToken")) { - return cookie; - } - } - return new Cookie("accessToken", null); - } - - private void addCookieToResponse(Cookie cookie, HttpServletResponse response) { - cookie.setHttpOnly(true); - - response.addCookie(cookie); - } -} diff --git a/src/main/java/roomescape/system/auth/dto/LoginCheckResponse.java b/src/main/java/roomescape/system/auth/dto/LoginCheckResponse.java deleted file mode 100644 index 957012cb..00000000 --- a/src/main/java/roomescape/system/auth/dto/LoginCheckResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package roomescape.system.auth.dto; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(name = "로그인 체크 응답", description = "로그인 상태 체크 응답시 사용됩니다.") -public record LoginCheckResponse( - @Schema(description = "로그인된 회원의 이름") String name -) { -} diff --git a/src/main/java/roomescape/system/auth/dto/LoginRequest.java b/src/main/java/roomescape/system/auth/dto/LoginRequest.java deleted file mode 100644 index 72a5125a..00000000 --- a/src/main/java/roomescape/system/auth/dto/LoginRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package roomescape.system.auth.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; - -@Schema(name = "로그인 요청", description = "로그인 요청 시 사용됩니다.") -public record LoginRequest( - @NotBlank(message = "이메일은 null 또는 공백일 수 없습니다.") - @Email(message = "이메일 형식이 일치하지 않습니다. 예시: abc123@gmail.com)") - @Schema(description = "필수 값이며, 이메일 형식으로 입력해야 합니다.", example = "abc123@gmail.com") - String email, - @NotBlank(message = "비밀번호는 null 또는 공백일 수 없습니다.") - @Schema(description = "최소 1글자 이상 입력해야 합니다.") - String password -) { -} diff --git a/src/main/java/roomescape/system/auth/infrastructure/jwt/JwtHandler.kt b/src/main/java/roomescape/system/auth/infrastructure/jwt/JwtHandler.kt new file mode 100644 index 00000000..4afd8e3e --- /dev/null +++ b/src/main/java/roomescape/system/auth/infrastructure/jwt/JwtHandler.kt @@ -0,0 +1,50 @@ +package roomescape.system.auth.infrastructure.jwt + +import io.jsonwebtoken.* +import org.springframework.beans.factory.annotation.Value +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component +import roomescape.system.exception.ErrorType +import roomescape.system.exception.RoomEscapeException +import java.util.* + +@Component +class JwtHandler( + @Value("\${security.jwt.token.secret-key}") + private val secretKey: String, + + @Value("\${security.jwt.token.access.expire-length}") + private val accessTokenExpireTime: Long +) { + fun createToken(memberId: Long): String { + val date = Date() + val accessTokenExpiredAt = Date(date.time + accessTokenExpireTime) + + return Jwts.builder() + .claim("memberId", memberId) + .setIssuedAt(date) + .setExpiration(accessTokenExpiredAt) + .signWith(SignatureAlgorithm.HS256, secretKey.toByteArray()) + .compact() + } + + fun getMemberIdFromToken(token: String?): Long { + try { + return Jwts.parser() + .setSigningKey(secretKey.toByteArray()) + .parseClaimsJws(token) + .getBody() + .get("memberId", Number::class.java) + .toLong() + } catch (e: Exception) { + when (e) { + is ExpiredJwtException -> throw RoomEscapeException(ErrorType.EXPIRED_TOKEN, HttpStatus.UNAUTHORIZED) + is UnsupportedJwtException -> throw RoomEscapeException(ErrorType.UNSUPPORTED_TOKEN, HttpStatus.UNAUTHORIZED) + is MalformedJwtException -> throw RoomEscapeException(ErrorType.MALFORMED_TOKEN, HttpStatus.UNAUTHORIZED) + is SignatureException -> throw RoomEscapeException(ErrorType.INVALID_SIGNATURE_TOKEN, HttpStatus.UNAUTHORIZED) + is IllegalArgumentException -> throw RoomEscapeException(ErrorType.INVALID_TOKEN, HttpStatus.UNAUTHORIZED) + else -> throw RoomEscapeException(ErrorType.UNEXPECTED_ERROR, HttpStatus.INTERNAL_SERVER_ERROR) + } + } + } +} diff --git a/src/main/java/roomescape/system/auth/interceptor/AdminInterceptor.java b/src/main/java/roomescape/system/auth/interceptor/AdminInterceptor.java deleted file mode 100644 index 4304a37f..00000000 --- a/src/main/java/roomescape/system/auth/interceptor/AdminInterceptor.java +++ /dev/null @@ -1,86 +0,0 @@ -package roomescape.system.auth.interceptor; - -import java.util.Arrays; - -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.servlet.HandlerInterceptor; - -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import roomescape.member.domain.Member; -import roomescape.member.service.MemberService; -import roomescape.system.auth.annotation.Admin; -import roomescape.system.auth.jwt.JwtHandler; -import roomescape.system.exception.ErrorType; -import roomescape.system.exception.RoomEscapeException; - -@Component -public class AdminInterceptor implements HandlerInterceptor { - - private static final String ACCESS_TOKEN_COOKIE_NAME = "accessToken"; - private final MemberService memberService; - private final JwtHandler jwtHandler; - - public AdminInterceptor(MemberService memberService, JwtHandler jwtHandler) { - this.memberService = memberService; - this.jwtHandler = jwtHandler; - } - - @Override - public boolean preHandle( - HttpServletRequest request, - HttpServletResponse response, - Object handler - ) - throws Exception { - if (isHandlerIrrelevantWithAdmin(handler)) { - return true; - } - - Member member; - try { - Cookie token = getToken(request); - Long memberId = jwtHandler.getMemberIdFromToken(token.getValue()); - member = memberService.findMemberById(memberId); - } catch (RoomEscapeException e) { - response.sendRedirect("/login"); - throw e; - } - - if (member.isAdmin()) { - return true; - } else { - response.sendRedirect("/login"); - throw new RoomEscapeException(ErrorType.PERMISSION_DOES_NOT_EXIST, - String.format("[memberId: %d, Role: %s]", member.getId(), member.getRole()), HttpStatus.FORBIDDEN); - } - } - - private Cookie getToken(HttpServletRequest request) { - validateCookieHeader(request); - - Cookie[] cookies = request.getCookies(); - return Arrays.stream(cookies) - .filter(cookie -> cookie.getName().equals(ACCESS_TOKEN_COOKIE_NAME)) - .findAny() - .orElseThrow(() -> new RoomEscapeException(ErrorType.INVALID_TOKEN, HttpStatus.UNAUTHORIZED)); - } - - private void validateCookieHeader(HttpServletRequest request) { - String cookieHeader = request.getHeader("Cookie"); - if (cookieHeader == null) { - throw new RoomEscapeException(ErrorType.NOT_EXIST_COOKIE, HttpStatus.UNAUTHORIZED); - } - } - - private boolean isHandlerIrrelevantWithAdmin(Object handler) { - if (!(handler instanceof HandlerMethod handlerMethod)) { - return true; - } - Admin adminAnnotation = handlerMethod.getMethodAnnotation(Admin.class); - return adminAnnotation == null; - } -} diff --git a/src/main/java/roomescape/system/auth/interceptor/LoginInterceptor.java b/src/main/java/roomescape/system/auth/interceptor/LoginInterceptor.java deleted file mode 100644 index 2aa0bafc..00000000 --- a/src/main/java/roomescape/system/auth/interceptor/LoginInterceptor.java +++ /dev/null @@ -1,79 +0,0 @@ -package roomescape.system.auth.interceptor; - -import java.util.Arrays; - -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.servlet.HandlerInterceptor; - -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import roomescape.member.domain.Member; -import roomescape.member.service.MemberService; -import roomescape.system.auth.annotation.LoginRequired; -import roomescape.system.auth.jwt.JwtHandler; -import roomescape.system.exception.ErrorType; -import roomescape.system.exception.RoomEscapeException; - -@Component -public class LoginInterceptor implements HandlerInterceptor { - - private static final String ACCESS_TOKEN_COOKIE_NAME = "accessToken"; - private final MemberService memberService; - private final JwtHandler jwtHandler; - - public LoginInterceptor(MemberService memberService, JwtHandler jwtHandler) { - this.memberService = memberService; - this.jwtHandler = jwtHandler; - } - - @Override - public boolean preHandle( - HttpServletRequest request, - HttpServletResponse response, - Object handler - ) - throws Exception { - if (isHandlerIrrelevantWithLoginRequired(handler)) { - return true; - } - - Member member; - try { - Cookie token = getToken(request); - Long memberId = jwtHandler.getMemberIdFromToken(token.getValue()); - member = memberService.findMemberById(memberId); - return member != null; - } catch (RoomEscapeException e) { - response.sendRedirect("/login"); - throw new RoomEscapeException(ErrorType.LOGIN_REQUIRED, HttpStatus.FORBIDDEN); - } - } - - private Cookie getToken(HttpServletRequest request) { - validateCookieHeader(request); - - Cookie[] cookies = request.getCookies(); - return Arrays.stream(cookies) - .filter(cookie -> cookie.getName().equals(ACCESS_TOKEN_COOKIE_NAME)) - .findAny() - .orElseThrow(() -> new RoomEscapeException(ErrorType.INVALID_TOKEN, HttpStatus.UNAUTHORIZED)); - } - - private void validateCookieHeader(HttpServletRequest request) { - String cookieHeader = request.getHeader("Cookie"); - if (cookieHeader == null) { - throw new RoomEscapeException(ErrorType.NOT_EXIST_COOKIE, HttpStatus.UNAUTHORIZED); - } - } - - private boolean isHandlerIrrelevantWithLoginRequired(Object handler) { - if (!(handler instanceof HandlerMethod handlerMethod)) { - return true; - } - LoginRequired loginRequiredAnnotation = handlerMethod.getMethodAnnotation(LoginRequired.class); - return loginRequiredAnnotation == null; - } -} \ No newline at end of file diff --git a/src/main/java/roomescape/system/auth/jwt/JwtHandler.java b/src/main/java/roomescape/system/auth/jwt/JwtHandler.java deleted file mode 100644 index d2f24a5f..00000000 --- a/src/main/java/roomescape/system/auth/jwt/JwtHandler.java +++ /dev/null @@ -1,65 +0,0 @@ -package roomescape.system.auth.jwt; - -import java.util.Date; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; - -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; -import io.jsonwebtoken.UnsupportedJwtException; -import roomescape.system.auth.jwt.dto.TokenDto; -import roomescape.system.exception.ErrorType; -import roomescape.system.exception.RoomEscapeException; - -@Component -public class JwtHandler { - - @Value("${security.jwt.token.secret-key}") - private String secretKey; - - @Value("${security.jwt.token.access.expire-length}") - private long accessTokenExpireTime; - - public TokenDto createToken(Long memberId) { - Date date = new Date(); - Date accessTokenExpiredAt = new Date(date.getTime() + accessTokenExpireTime); - - String accessToken = Jwts.builder() - .claim("memberId", memberId) - .setIssuedAt(date) - .setExpiration(accessTokenExpiredAt) - .signWith(SignatureAlgorithm.HS256, secretKey.getBytes()) - .compact(); - - return new TokenDto(accessToken); - } - - public Long getMemberIdFromToken(String token) { - validateToken(token); - - return Jwts.parser().setSigningKey(secretKey.getBytes()).parseClaimsJws(token) - .getBody() - .get("memberId", Long.class); - } - - public void validateToken(String token) { - try { - Jwts.parser().setSigningKey(secretKey.getBytes()).parseClaimsJws(token); - } catch (ExpiredJwtException e) { - throw new RoomEscapeException(ErrorType.EXPIRED_TOKEN, HttpStatus.UNAUTHORIZED); - } catch (UnsupportedJwtException e) { - throw new RoomEscapeException(ErrorType.UNSUPPORTED_TOKEN, HttpStatus.UNAUTHORIZED); - } catch (MalformedJwtException e) { - throw new RoomEscapeException(ErrorType.MALFORMED_TOKEN, HttpStatus.UNAUTHORIZED); - } catch (SignatureException e) { - throw new RoomEscapeException(ErrorType.INVALID_SIGNATURE_TOKEN, HttpStatus.UNAUTHORIZED); - } catch (IllegalArgumentException e) { - throw new RoomEscapeException(ErrorType.ILLEGAL_TOKEN, HttpStatus.UNAUTHORIZED); - } - } -} diff --git a/src/main/java/roomescape/system/auth/jwt/dto/TokenDto.java b/src/main/java/roomescape/system/auth/jwt/dto/TokenDto.java deleted file mode 100644 index cd6302de..00000000 --- a/src/main/java/roomescape/system/auth/jwt/dto/TokenDto.java +++ /dev/null @@ -1,4 +0,0 @@ -package roomescape.system.auth.jwt.dto; - -public record TokenDto(String accessToken) { -} diff --git a/src/main/java/roomescape/system/auth/resolver/MemberIdResolver.java b/src/main/java/roomescape/system/auth/resolver/MemberIdResolver.java deleted file mode 100644 index d72c3d55..00000000 --- a/src/main/java/roomescape/system/auth/resolver/MemberIdResolver.java +++ /dev/null @@ -1,53 +0,0 @@ -package roomescape.system.auth.resolver; - -import java.util.Arrays; - -import org.springframework.core.MethodParameter; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; -import org.springframework.web.bind.support.WebDataBinderFactory; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.method.support.ModelAndViewContainer; - -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import roomescape.system.auth.annotation.MemberId; -import roomescape.system.auth.jwt.JwtHandler; -import roomescape.system.exception.ErrorType; -import roomescape.system.exception.RoomEscapeException; - -@Component -public class MemberIdResolver implements HandlerMethodArgumentResolver { - - private static final String ACCESS_TOKEN_COOKIE_NAME = "accessToken"; - - private final JwtHandler jwtHandler; - - public MemberIdResolver(JwtHandler jwtHandler) { - this.jwtHandler = jwtHandler; - } - - @Override - public boolean supportsParameter(MethodParameter parameter) { - return parameter.hasParameterAnnotation(MemberId.class); - } - - @Override - public Object resolveArgument( - MethodParameter parameter, - ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, - WebDataBinderFactory binderFactory - ) throws Exception { - Cookie[] cookies = webRequest.getNativeRequest(HttpServletRequest.class).getCookies(); - if (cookies == null) { - throw new RoomEscapeException(ErrorType.NOT_EXIST_COOKIE, HttpStatus.UNAUTHORIZED); - } - return Arrays.stream(cookies) - .filter(cookie -> cookie.getName().equals(ACCESS_TOKEN_COOKIE_NAME)) - .findAny() - .map(cookie -> jwtHandler.getMemberIdFromToken(cookie.getValue())) - .orElseThrow(() -> new RoomEscapeException(ErrorType.INVALID_TOKEN, HttpStatus.UNAUTHORIZED)); - } -} diff --git a/src/main/java/roomescape/system/auth/service/AuthService.java b/src/main/java/roomescape/system/auth/service/AuthService.java deleted file mode 100644 index e8d43828..00000000 --- a/src/main/java/roomescape/system/auth/service/AuthService.java +++ /dev/null @@ -1,34 +0,0 @@ -package roomescape.system.auth.service; - -import org.springframework.stereotype.Service; - -import roomescape.member.domain.Member; -import roomescape.member.service.MemberService; -import roomescape.system.auth.dto.LoginCheckResponse; -import roomescape.system.auth.dto.LoginRequest; -import roomescape.system.auth.jwt.JwtHandler; -import roomescape.system.auth.jwt.dto.TokenDto; - -@Service -public class AuthService { - - private final MemberService memberService; - private final JwtHandler jwtHandler; - - public AuthService(MemberService memberService, JwtHandler jwtHandler) { - this.memberService = memberService; - this.jwtHandler = jwtHandler; - } - - public TokenDto login(LoginRequest request) { - Member member = memberService.findMemberByEmailAndPassword(request.email(), request.password()); - - return jwtHandler.createToken(member.getId()); - } - - public LoginCheckResponse checkLogin(Long memberId) { - Member member = memberService.findMemberById(memberId); - - return new LoginCheckResponse(member.getName()); - } -} diff --git a/src/main/java/roomescape/system/auth/service/AuthService.kt b/src/main/java/roomescape/system/auth/service/AuthService.kt new file mode 100644 index 00000000..b6394130 --- /dev/null +++ b/src/main/java/roomescape/system/auth/service/AuthService.kt @@ -0,0 +1,32 @@ +package roomescape.system.auth.service + +import org.springframework.stereotype.Service +import roomescape.member.business.MemberService +import roomescape.member.infrastructure.persistence.Member +import roomescape.system.auth.infrastructure.jwt.JwtHandler +import roomescape.system.auth.web.LoginCheckResponse +import roomescape.system.auth.web.LoginRequest +import roomescape.system.auth.web.TokenResponse + +@Service +class AuthService( + private val memberService: MemberService, + private val jwtHandler: JwtHandler +) { + fun login(request: LoginRequest): TokenResponse { + val member: Member = memberService.findMemberByEmailAndPassword( + request.email, + request.password + ) + + val accessToken: String = jwtHandler.createToken(member.id!!) + + return TokenResponse(accessToken) + } + + fun checkLogin(memberId: Long): LoginCheckResponse { + val member = memberService.findById(memberId) + + return LoginCheckResponse(member.name) + } +} diff --git a/src/main/java/roomescape/system/auth/web/AuthAPI.kt b/src/main/java/roomescape/system/auth/web/AuthAPI.kt new file mode 100644 index 00000000..27dd2ecd --- /dev/null +++ b/src/main/java/roomescape/system/auth/web/AuthAPI.kt @@ -0,0 +1,67 @@ +package roomescape.system.auth.web + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.media.Content +import io.swagger.v3.oas.annotations.media.Schema +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import io.swagger.v3.oas.annotations.tags.Tag +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import jakarta.validation.Valid +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.ResponseStatus +import roomescape.system.auth.web.support.LoginRequired +import roomescape.system.auth.web.support.MemberId +import roomescape.system.dto.response.ErrorResponse +import roomescape.system.dto.response.RoomEscapeApiResponse + +@Tag(name = "1. 인증 / 인가 API", description = "로그인, 로그아웃 및 로그인 상태를 확인합니다") +interface AuthAPI { + + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "로그인") + @ApiResponses( + ApiResponse( + responseCode = "200", + description = "로그인 성공시 쿠키에 토큰 정보를 저장합니다." + ), + ApiResponse( + responseCode = "400", + description = "존재하지 않는 회원이거나, 이메일 또는 비밀번호가 잘못 입력되었습니다.", + content = [Content(schema = Schema(implementation = ErrorResponse::class))] + ) + ) + fun login( + @Valid @RequestBody loginRequest: LoginRequest, + response: HttpServletResponse + ): RoomEscapeApiResponse + + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "로그인 상태 확인") + @ApiResponses( + ApiResponse( + responseCode = "200", + description = "로그인 상태이며, 로그인된 회원의 이름을 반환합니다." + ), + ApiResponse( + responseCode = "400", + description = "쿠키에 있는 토큰 정보로 회원을 조회할 수 없습니다.", + content = [Content(schema = Schema(implementation = ErrorResponse::class))] + ), + ApiResponse( + responseCode = "401", + description = "토큰 정보가 없거나, 만료되었습니다.", + content = [Content(schema = Schema(implementation = ErrorResponse::class))] + ) + ) + fun checkLogin(@MemberId @Parameter(hidden = true) memberId: Long): RoomEscapeApiResponse + + @LoginRequired + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "로그아웃", tags = ["로그인이 필요한 API"]) + @ApiResponses(ApiResponse(responseCode = "200", description = "로그아웃 성공시 쿠키에 저장된 토큰 정보를 삭제합니다.")) + fun logout(request: HttpServletRequest, response: HttpServletResponse): RoomEscapeApiResponse +} diff --git a/src/main/java/roomescape/system/auth/web/AuthController.kt b/src/main/java/roomescape/system/auth/web/AuthController.kt new file mode 100644 index 00000000..2008a124 --- /dev/null +++ b/src/main/java/roomescape/system/auth/web/AuthController.kt @@ -0,0 +1,54 @@ +package roomescape.system.auth.web + +import io.swagger.v3.oas.annotations.Parameter +import jakarta.servlet.http.Cookie +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController +import roomescape.system.auth.service.AuthService +import roomescape.system.auth.web.support.* +import roomescape.system.dto.response.RoomEscapeApiResponse + +@RestController +class AuthController( + private val authService: AuthService +) : AuthAPI { + + @PostMapping("/login") + override fun login( + @Valid @RequestBody loginRequest: LoginRequest, + response: HttpServletResponse + ): RoomEscapeApiResponse { + val accessToken: TokenResponse = authService.login(loginRequest) + val cookie: Cookie = accessToken.toCookie() + + response.addAccessTokenCookie(cookie) + + return RoomEscapeApiResponse.success() + } + + @GetMapping("/login/check") + override fun checkLogin( + @MemberId @Parameter(hidden = true) memberId: Long + ): RoomEscapeApiResponse { + val response = authService.checkLogin(memberId) + + return RoomEscapeApiResponse.success(response) + } + + @PostMapping("/logout") + override fun logout( + request: HttpServletRequest, + response: HttpServletResponse + ): RoomEscapeApiResponse { + val cookie: Cookie = request.accessTokenCookie() + cookie.expire() + response.addAccessTokenCookie(cookie) + + return RoomEscapeApiResponse.success() + } +} diff --git a/src/main/java/roomescape/system/auth/web/AuthDTO.kt b/src/main/java/roomescape/system/auth/web/AuthDTO.kt new file mode 100644 index 00000000..05ef0b32 --- /dev/null +++ b/src/main/java/roomescape/system/auth/web/AuthDTO.kt @@ -0,0 +1,30 @@ +package roomescape.system.auth.web + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.Email +import jakarta.validation.constraints.NotBlank + +@JvmRecord +data class TokenResponse( + val accessToken: String +) + + +@Schema(name = "로그인 체크 응답", description = "로그인 상태 체크 응답시 사용됩니다.") +@JvmRecord +data class LoginCheckResponse( + @field:Schema(description = "로그인된 회원의 이름") + val name: String +) + +@Schema(name = "로그인 요청", description = "로그인 요청 시 사용됩니다.") +@JvmRecord +data class LoginRequest( + @Email(message = "이메일 형식이 일치하지 않습니다. 예시: abc123@gmail.com") + @field:Schema(description = "필수 값이며, 이메일 형식으로 입력해야 합니다.", example = "abc123@gmail.com") + val email: String, + + @NotBlank(message = "비밀번호는 공백일 수 없습니다.") + @field:Schema(description = "최소 1글자 이상 입력해야 합니다.") + val password: String +) diff --git a/src/main/java/roomescape/system/auth/web/support/AuthAnnotations.kt b/src/main/java/roomescape/system/auth/web/support/AuthAnnotations.kt new file mode 100644 index 00000000..77bb3090 --- /dev/null +++ b/src/main/java/roomescape/system/auth/web/support/AuthAnnotations.kt @@ -0,0 +1,13 @@ +package roomescape.system.auth.web.support + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class Admin + +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class LoginRequired + +@Target(AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +annotation class MemberId \ No newline at end of file diff --git a/src/main/java/roomescape/system/auth/web/support/AuthInterceptors.kt b/src/main/java/roomescape/system/auth/web/support/AuthInterceptors.kt new file mode 100644 index 00000000..7ef82712 --- /dev/null +++ b/src/main/java/roomescape/system/auth/web/support/AuthInterceptors.kt @@ -0,0 +1,90 @@ +package roomescape.system.auth.web.support + +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component +import org.springframework.web.method.HandlerMethod +import org.springframework.web.servlet.HandlerInterceptor +import roomescape.member.business.MemberService +import roomescape.member.infrastructure.persistence.Member +import roomescape.system.auth.infrastructure.jwt.JwtHandler +import roomescape.system.exception.ErrorType +import roomescape.system.exception.RoomEscapeException + +private fun Any.isIrrelevantWith(annotationType: Class): Boolean { + if (this !is HandlerMethod) { + return true + } + return !this.hasMethodAnnotation(annotationType) +} + +@Component +class LoginInterceptor( + private val memberService: MemberService, + private val jwtHandler: JwtHandler +) : HandlerInterceptor { + + @Throws(Exception::class) + override fun preHandle( + request: HttpServletRequest, + response: HttpServletResponse, + handler: Any + ): Boolean { + if (handler.isIrrelevantWith(LoginRequired::class.java)) { + return true + } + + try { + val token: String? = request.accessTokenCookie().value + val memberId: Long = jwtHandler.getMemberIdFromToken(token) + + return memberService.existsById(memberId) + } catch (e: RoomEscapeException) { + response.sendRedirect("/login") + throw RoomEscapeException(ErrorType.LOGIN_REQUIRED, HttpStatus.FORBIDDEN) + } + } +} + +@Component +class AdminInterceptor( + private val memberService: MemberService, + private val jwtHandler: JwtHandler +) : HandlerInterceptor { + + @Throws(Exception::class) + override fun preHandle( + request: HttpServletRequest, + response: HttpServletResponse, + handler: Any + ): Boolean { + if (handler.isIrrelevantWith(Admin::class.java)) { + return true + } + + val member: Member? + + try { + val token: String? = request.accessTokenCookie().value + val memberId: Long = jwtHandler.getMemberIdFromToken(token) + member = memberService.findById(memberId) + } catch (e: RoomEscapeException) { + response.sendRedirect("/login") + throw e + } + + with(member) { + if (this.isAdmin()) { + return true + } + + response.sendRedirect("/login") + throw RoomEscapeException( + ErrorType.PERMISSION_DOES_NOT_EXIST, + String.format("[memberId: %d, Role: %s]", this.id, this.role), + HttpStatus.FORBIDDEN + ) + } + } +} diff --git a/src/main/java/roomescape/system/auth/web/support/CookieUtils.kt b/src/main/java/roomescape/system/auth/web/support/CookieUtils.kt new file mode 100644 index 00000000..77d33b44 --- /dev/null +++ b/src/main/java/roomescape/system/auth/web/support/CookieUtils.kt @@ -0,0 +1,27 @@ +package roomescape.system.auth.web.support + +import jakarta.servlet.http.Cookie +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import roomescape.system.auth.web.TokenResponse + +const val ACCESS_TOKEN_COOKIE_NAME = "accessToken" + +fun Cookie.expire(): Unit { + this.value = "" + this.maxAge = 0 +} + +fun TokenResponse.toCookie(): Cookie = Cookie(ACCESS_TOKEN_COOKIE_NAME, this.accessToken) + .also { it.maxAge = 1800000 } + +fun HttpServletRequest.accessTokenCookie(): Cookie = this.cookies + ?.firstOrNull { it.name == ACCESS_TOKEN_COOKIE_NAME } + ?: Cookie(ACCESS_TOKEN_COOKIE_NAME, "") + +fun HttpServletResponse.addAccessTokenCookie(cookie: Cookie) { + cookie.isHttpOnly = true + cookie.secure = true + cookie.path = "/" + this.addCookie(cookie) +} diff --git a/src/main/java/roomescape/system/auth/web/support/MemberIdResolver.kt b/src/main/java/roomescape/system/auth/web/support/MemberIdResolver.kt new file mode 100644 index 00000000..a22d5fc5 --- /dev/null +++ b/src/main/java/roomescape/system/auth/web/support/MemberIdResolver.kt @@ -0,0 +1,33 @@ +package roomescape.system.auth.web.support + +import jakarta.servlet.http.HttpServletRequest +import org.springframework.core.MethodParameter +import org.springframework.stereotype.Component +import org.springframework.web.bind.support.WebDataBinderFactory +import org.springframework.web.context.request.NativeWebRequest +import org.springframework.web.method.support.HandlerMethodArgumentResolver +import org.springframework.web.method.support.ModelAndViewContainer +import roomescape.system.auth.infrastructure.jwt.JwtHandler + +@Component +class MemberIdResolver( + private val jwtHandler: JwtHandler +) : HandlerMethodArgumentResolver { + + override fun supportsParameter(parameter: MethodParameter): Boolean { + return parameter.hasParameterAnnotation(MemberId::class.java) + } + + @Throws(Exception::class) + override fun resolveArgument( + parameter: MethodParameter, + mavContainer: ModelAndViewContainer?, + webRequest: NativeWebRequest, + binderFactory: WebDataBinderFactory? + ): Any { + val request: HttpServletRequest = webRequest.nativeRequest as HttpServletRequest + val token: String = request.accessTokenCookie().value + + return jwtHandler.getMemberIdFromToken(token) + } +} diff --git a/src/main/java/roomescape/system/config/WebMvcConfig.java b/src/main/java/roomescape/system/config/WebMvcConfig.java index 000349bc..9c286cab 100644 --- a/src/main/java/roomescape/system/config/WebMvcConfig.java +++ b/src/main/java/roomescape/system/config/WebMvcConfig.java @@ -7,9 +7,9 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import roomescape.system.auth.interceptor.AdminInterceptor; -import roomescape.system.auth.interceptor.LoginInterceptor; -import roomescape.system.auth.resolver.MemberIdResolver; +import roomescape.system.auth.web.support.AdminInterceptor; +import roomescape.system.auth.web.support.LoginInterceptor; +import roomescape.system.auth.web.support.MemberIdResolver; @Configuration public class WebMvcConfig implements WebMvcConfigurer { diff --git a/src/main/java/roomescape/system/exception/ErrorType.java b/src/main/java/roomescape/system/exception/ErrorType.java index ab026a3f..b99be8d1 100644 --- a/src/main/java/roomescape/system/exception/ErrorType.java +++ b/src/main/java/roomescape/system/exception/ErrorType.java @@ -43,6 +43,7 @@ public enum ErrorType { // 500 Internal Server Error, INTERNAL_SERVER_ERROR("서버 내부에서 에러가 발생하였습니다."), + UNEXPECTED_ERROR("예상치 못한 에러가 발생하였습니다. 잠시 후 다시 시도해주세요."), // Payment Error PAYMENT_ERROR("결제(취소)에 실패했습니다. 결제(취소) 정보를 확인해주세요."), diff --git a/src/main/java/roomescape/theme/controller/ThemeController.java b/src/main/java/roomescape/theme/controller/ThemeController.java index d57f9da4..0c44bf4c 100644 --- a/src/main/java/roomescape/theme/controller/ThemeController.java +++ b/src/main/java/roomescape/theme/controller/ThemeController.java @@ -21,8 +21,8 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; -import roomescape.system.auth.annotation.Admin; -import roomescape.system.auth.annotation.LoginRequired; +import roomescape.system.auth.web.support.Admin; +import roomescape.system.auth.web.support.LoginRequired; import roomescape.system.dto.response.ErrorResponse; import roomescape.system.dto.response.RoomEscapeApiResponse; import roomescape.theme.dto.ThemeRequest; diff --git a/src/main/java/roomescape/view/controller/PageController.kt b/src/main/java/roomescape/view/controller/PageController.kt index 5bbe9afc..62504e7e 100644 --- a/src/main/java/roomescape/view/controller/PageController.kt +++ b/src/main/java/roomescape/view/controller/PageController.kt @@ -4,8 +4,8 @@ import org.springframework.stereotype.Controller import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestMapping -import roomescape.system.auth.annotation.Admin -import roomescape.system.auth.annotation.LoginRequired +import roomescape.system.auth.web.support.Admin +import roomescape.system.auth.web.support.LoginRequired @Controller class AuthPageController { diff --git a/src/test/java/roomescape/common/Fixtures.kt b/src/test/java/roomescape/common/Fixtures.kt index 0eb6f562..b53e7f3e 100644 --- a/src/test/java/roomescape/common/Fixtures.kt +++ b/src/test/java/roomescape/common/Fixtures.kt @@ -1,7 +1,9 @@ package roomescape.common -import roomescape.member.domain.Member -import roomescape.member.domain.Role +import roomescape.member.infrastructure.persistence.Member +import roomescape.member.infrastructure.persistence.Role +import roomescape.system.auth.infrastructure.jwt.JwtHandler +import roomescape.system.auth.web.LoginRequest import java.util.concurrent.atomic.AtomicLong object MemberFixture { @@ -16,5 +18,24 @@ object MemberFixture { ): Member = Member(id, name, "$account@email.com", password, role) fun admin(): Member = create(account = "admin", role = Role.ADMIN) + fun adminLoginRequest(): LoginRequest = LoginRequest( + email = admin().email, + password = admin().password + ) + fun user(): Member = create(account = "user", role = Role.MEMBER) + fun userLoginRequest(): LoginRequest = LoginRequest( + email = user().email, + password = user().password + ) +} + +object JwtFixture { + const val SECRET_KEY: String = "daijawligagaf@LIJ$@U)9nagnalkkgalijaddljfi" + const val EXPIRATION_TIME: Long = 1000 * 60 * 60 + + fun create( + secretKey: String = SECRET_KEY, + expirationTime: Long = EXPIRATION_TIME + ): JwtHandler = JwtHandler(secretKey, expirationTime) } diff --git a/src/test/java/roomescape/common/RoomescapeApiTest.kt b/src/test/java/roomescape/common/RoomescapeApiTest.kt new file mode 100644 index 00000000..6c4701ce --- /dev/null +++ b/src/test/java/roomescape/common/RoomescapeApiTest.kt @@ -0,0 +1,93 @@ +package roomescape.common + +import com.ninjasquad.springmockk.MockkBean +import io.kotest.core.spec.style.BehaviorSpec +import io.mockk.every +import io.restassured.http.ContentType +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import io.restassured.response.ValidatableResponse +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.data.repository.findByIdOrNull +import org.springframework.http.HttpStatus +import roomescape.member.infrastructure.persistence.Member +import roomescape.member.infrastructure.persistence.MemberRepository +import roomescape.system.auth.infrastructure.jwt.JwtHandler +import roomescape.system.exception.ErrorType +import roomescape.system.exception.RoomEscapeException + +const val NOT_LOGGED_IN_USERID: Long = 0; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@NoSqlInitialize +class RoomescapeApiTest( + @LocalServerPort val port: Int? = 9090, +) : BehaviorSpec() { + + @MockkBean + lateinit var memberRepository: MemberRepository + + @MockkBean + lateinit var jwtHandler: JwtHandler + + + val admin: Member = MemberFixture.admin() + val user: Member = MemberFixture.user() + + fun runGetTest(endpoint: String, token: String? = "token", assert: ValidatableResponse.() -> Unit): ValidatableResponse { + return Given { + port(port!!) + header("Cookie", "accessToken=$token") + } When { + get(endpoint) + } Then assert + } + + fun runPostTest( + endpoint: String, + token: String? = "token", + body: Any? = null, + assert: ValidatableResponse.() -> Unit + ): ValidatableResponse { + return Given { + port(port!!) + contentType(ContentType.JSON) + body?.let { body(it) } + header("Cookie", "accessToken=$token") + } When { + post(endpoint) + } Then assert + } + + fun setUpAdmin() { + every { + jwtHandler.getMemberIdFromToken(any()) + } returns admin.id!! + + every { memberRepository.existsById(admin.id!!) } returns true + every { memberRepository.findByIdOrNull(admin.id!!) } returns admin + } + + fun setUpUser() { + every { + jwtHandler.getMemberIdFromToken(any()) + } returns user.id!! + + every { memberRepository.existsById(user.id!!) } returns true + every { memberRepository.findByIdOrNull(user.id!!) } returns user + } + + fun setUpNotLoggedIn() { + every { + jwtHandler.getMemberIdFromToken(any()) + } returns NOT_LOGGED_IN_USERID + + every { memberRepository.existsById(NOT_LOGGED_IN_USERID) } throws RoomEscapeException( + ErrorType.LOGIN_REQUIRED, + HttpStatus.FORBIDDEN + ) + every { memberRepository.findByIdOrNull(NOT_LOGGED_IN_USERID) } returns null + } +} diff --git a/src/test/java/roomescape/global/auth/jwt/JwtHandlerTest.java b/src/test/java/roomescape/global/auth/jwt/JwtHandlerTest.java deleted file mode 100644 index 8b22eb86..00000000 --- a/src/test/java/roomescape/global/auth/jwt/JwtHandlerTest.java +++ /dev/null @@ -1,118 +0,0 @@ -package roomescape.global.auth.jwt; - -import static roomescape.system.exception.ErrorType.*; - -import java.util.Date; - -import org.assertj.core.api.Assertions; -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.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.test.context.jdbc.Sql; - -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.restassured.RestAssured; -import roomescape.system.auth.jwt.JwtHandler; -import roomescape.system.exception.ErrorType; -import roomescape.system.exception.RoomEscapeException; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Sql(scripts = "/truncate.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) -class JwtHandlerTest { - - @Autowired - private JwtHandler jwtHandler; - - @Value("${security.jwt.token.secret-key}") - private String secretKey; - - @LocalServerPort - private int port; - - @BeforeEach - void setUp() { - RestAssured.port = port; - } - - @Test - @DisplayName("토큰이 만료되면 401 Unauthorized 를 발생시킨다.") - void jwtExpired() { - //given - Date date = new Date(); - Date expiredAt = new Date(date.getTime() - 1); - - String accessToken = Jwts.builder() - .claim("memberId", 1L) - .setIssuedAt(date) - .setExpiration(expiredAt) - .signWith(SignatureAlgorithm.HS256, secretKey.getBytes()) - .compact(); - - // when & then - Assertions.assertThatThrownBy(() -> jwtHandler.getMemberIdFromToken(accessToken)) - .isInstanceOf(RoomEscapeException.class) - .hasMessage(EXPIRED_TOKEN.getDescription()); - } - - @Test - @DisplayName("지원하지 않는 토큰이면 401 Unauthorized 를 발생시킨다.") - void jwtMalformed() { - // given - Date date = new Date(); - Date expiredAt = new Date(date.getTime() + 100000); - - String accessToken = Jwts.builder() - .claim("memberId", 1L) - .setIssuedAt(date) - .setExpiration(expiredAt) - .signWith(SignatureAlgorithm.HS256, secretKey.getBytes()) - .compact(); - - String[] splitAccessToken = accessToken.split("\\."); - String unsupportedAccessToken = splitAccessToken[0] + "." + splitAccessToken[1]; - - // when & then - Assertions.assertThatThrownBy(() -> jwtHandler.getMemberIdFromToken(unsupportedAccessToken)) - .isInstanceOf(RoomEscapeException.class) - .hasMessage(ErrorType.MALFORMED_TOKEN.getDescription()); - } - - @Test - @DisplayName("토큰의 Signature 가 잘못되었다면 401 Unauthorized 를 발생시킨다.") - void jwtInvalidSignature() { - // given - Date date = new Date(); - Date expiredAt = new Date(date.getTime() + 100000); - - String invalidSecretKey = secretKey.substring(1); - - String accessToken = Jwts.builder() - .claim("memberId", 1L) - .setIssuedAt(date) - .setExpiration(expiredAt) - .signWith(SignatureAlgorithm.HS256, invalidSecretKey.getBytes()) // 기존은 HS256 알고리즘 - .compact(); - - // when & then - Assertions.assertThatThrownBy(() -> jwtHandler.getMemberIdFromToken(accessToken)) - .isInstanceOf(RoomEscapeException.class) - .hasMessage(ErrorType.INVALID_SIGNATURE_TOKEN.getDescription()); - } - - @Test - @DisplayName("토큰이 공백값이라면 401 Unauthorized 를 발생시킨다.") - void jwtIllegal() { - // given - String accessToken = ""; - - // when & then - Assertions.assertThatThrownBy(() -> jwtHandler.getMemberIdFromToken(accessToken)) - .isInstanceOf(RoomEscapeException.class) - .hasMessage(ErrorType.ILLEGAL_TOKEN.getDescription()); - } -} diff --git a/src/test/java/roomescape/member/controller/MemberControllerTest.java b/src/test/java/roomescape/member/controller/MemberControllerTest.java deleted file mode 100644 index 5cf989d9..00000000 --- a/src/test/java/roomescape/member/controller/MemberControllerTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package roomescape.member.controller; - -import java.util.Map; - -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.boot.test.web.server.LocalServerPort; -import org.springframework.test.context.jdbc.Sql; - -import io.restassured.RestAssured; -import io.restassured.http.ContentType; -import io.restassured.http.Header; -import roomescape.member.domain.Member; -import roomescape.member.domain.Role; -import roomescape.member.domain.repository.MemberRepository; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Sql(scripts = "/truncate.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) -class MemberControllerTest { - - @Autowired - private MemberRepository memberRepository; - - @LocalServerPort - private int port; - - @BeforeEach - void setUp() { - RestAssured.port = port; - } - - @Test - @DisplayName("/members 로 GET 요청을 보내면 회원 정보와 200 OK 를 받는다.") - void getAdminPage() { - // given - String accessTokenCookie = getAdminAccessTokenCookieByLogin("admin@admin.com", "12341234"); - - memberRepository.save(new Member("이름1", "test@test.com", "password", Role.MEMBER)); - memberRepository.save(new Member("이름2", "test@test.com", "password", Role.MEMBER)); - memberRepository.save(new Member("이름3", "test@test.com", "password", Role.MEMBER)); - memberRepository.save(new Member("이름4", "test@test.com", "password", Role.MEMBER)); - - // when & then - RestAssured.given().log().all() - .port(port) - .header(new Header("Cookie", accessTokenCookie)) - .when().get("/members") - .then().log().all() - .statusCode(200); - } - - private String getAdminAccessTokenCookieByLogin(final String email, final String password) { - memberRepository.save(new Member("이름", email, password, Role.ADMIN)); - - Map loginParams = Map.of( - "email", email, - "password", password - ); - - String accessToken = RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .body(loginParams) - .when().post("/login") - .then().log().all().extract().cookie("accessToken"); - - return "accessToken=" + accessToken; - } -} diff --git a/src/test/java/roomescape/member/controller/MemberControllerTest.kt b/src/test/java/roomescape/member/controller/MemberControllerTest.kt new file mode 100644 index 00000000..f268c327 --- /dev/null +++ b/src/test/java/roomescape/member/controller/MemberControllerTest.kt @@ -0,0 +1,66 @@ +package roomescape.member.controller + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.collections.shouldContainAll +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.restassured.module.kotlin.extensions.Extract +import org.hamcrest.Matchers.containsString +import roomescape.common.MemberFixture +import roomescape.common.RoomescapeApiTest +import roomescape.member.web.MembersResponse + +class MemberControllerTest : RoomescapeApiTest() { + + init { + given("GET /members 요청을") { + val endpoint = "/members" + + every { memberRepository.findAll() } returns listOf( + MemberFixture.create(name = "name1"), + MemberFixture.create(name = "name2"), + MemberFixture.create(name = "name3"), + ) + + `when`("관리자가 보내면") { + setUpAdmin() + + then("성공한다.") { + val result: Any = runGetTest(endpoint) { + statusCode(200) + } Extract { + path("data") + } + + val response: MembersResponse = jacksonObjectMapper().convertValue(result, MembersResponse::class.java) + + assertSoftly(response.members) { + it.size shouldBe 3 + it.map { m -> m.name } shouldContainAll listOf("name1", "name2", "name3") + } + } + } + + `when`("관리자가 아니면 로그인 페이지로 이동한다.") { + then("비회원") { + setUpNotLoggedIn() + + runGetTest(endpoint) { + statusCode(200) + body(containsString("Login")) + } + } + + then("일반 회원") { + setUpUser() + + runGetTest(endpoint) { + statusCode(200) + body(containsString("Login")) + } + } + } + } + } +} diff --git a/src/test/java/roomescape/member/domain/MemberTest.java b/src/test/java/roomescape/member/domain/MemberTest.java deleted file mode 100644 index 666982fd..00000000 --- a/src/test/java/roomescape/member/domain/MemberTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package roomescape.member.domain; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import roomescape.system.exception.RoomEscapeException; - -class MemberTest { - - @Test - @DisplayName("Member 객체를 생성할 때 Role은 반드시 입력되어야 한다.") - void createMemberWithoutRole() { - // given - String name = "name"; - String email = "email"; - String password = "password"; - - // when - Role role = null; - - // then - Assertions.assertThatThrownBy(() -> new Member(name, email, password, null)) - .isInstanceOf(RoomEscapeException.class); - } -} diff --git a/src/test/java/roomescape/payment/domain/PaymentTest.java b/src/test/java/roomescape/payment/domain/PaymentTest.java index 3c6e1fbc..1544cbb0 100644 --- a/src/test/java/roomescape/payment/domain/PaymentTest.java +++ b/src/test/java/roomescape/payment/domain/PaymentTest.java @@ -13,8 +13,8 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.NullSource; -import roomescape.member.domain.Member; -import roomescape.member.domain.Role; +import roomescape.member.infrastructure.persistence.Member; +import roomescape.member.infrastructure.persistence.Role; import roomescape.reservation.domain.Reservation; import roomescape.reservation.domain.ReservationStatus; import roomescape.reservation.domain.ReservationTime; @@ -30,7 +30,7 @@ class PaymentTest { LocalDate now = LocalDate.now(); ReservationTime reservationTime = new ReservationTime(LocalTime.now()); Theme theme = new Theme("name", "desc", "thumb"); - Member member = new Member("name", "email", "password", Role.MEMBER); + Member member = new Member(null, "name", "email", "password", Role.MEMBER); reservation = new Reservation(now, reservationTime, theme, member, ReservationStatus.CONFIRMED); } diff --git a/src/test/java/roomescape/payment/service/PaymentServiceTest.java b/src/test/java/roomescape/payment/service/PaymentServiceTest.java index 29a7ad75..66719b95 100644 --- a/src/test/java/roomescape/payment/service/PaymentServiceTest.java +++ b/src/test/java/roomescape/payment/service/PaymentServiceTest.java @@ -13,9 +13,9 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql.ExecutionPhase; -import roomescape.member.domain.Member; -import roomescape.member.domain.Role; -import roomescape.member.domain.repository.MemberRepository; +import roomescape.member.infrastructure.persistence.Member; +import roomescape.member.infrastructure.persistence.MemberRepository; +import roomescape.member.infrastructure.persistence.Role; import roomescape.payment.domain.repository.CanceledPaymentRepository; import roomescape.payment.dto.request.PaymentCancelRequest; import roomescape.payment.dto.response.PaymentResponse; @@ -54,7 +54,7 @@ class PaymentServiceTest { LocalDateTime localDateTime = LocalDateTime.now().plusHours(1L); LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); - Member member = memberRepository.save(new Member("member", "email@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER)); Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); Reservation reservation = reservationRepository.save(new Reservation(date, time, theme, member, ReservationStatus.CONFIRMED)); @@ -75,7 +75,7 @@ class PaymentServiceTest { LocalDateTime localDateTime = LocalDateTime.now().plusHours(1L); LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); - Member member = memberRepository.save(new Member("member", "email@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER)); Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); Reservation reservation = reservationRepository.save(new Reservation(date, time, theme, member, ReservationStatus.CONFIRMED)); @@ -111,7 +111,7 @@ class PaymentServiceTest { LocalDateTime localDateTime = LocalDateTime.now().plusHours(1L); LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); - Member member = memberRepository.save(new Member("member", "email@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER)); Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); Reservation reservation = reservationRepository.save(new Reservation(date, time, theme, member, ReservationStatus.CONFIRMED)); diff --git a/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java b/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java index ffba15b6..b5f625b1 100644 --- a/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java +++ b/src/test/java/roomescape/reservation/controller/ReservationControllerTest.java @@ -2,7 +2,6 @@ package roomescape.reservation.controller; import static org.assertj.core.api.Assertions.*; import static org.hamcrest.Matchers.*; -import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -33,9 +32,9 @@ import org.springframework.test.context.jdbc.Sql.ExecutionPhase; import io.restassured.RestAssured; import io.restassured.http.ContentType; import io.restassured.http.Header; -import roomescape.member.domain.Member; -import roomescape.member.domain.Role; -import roomescape.member.domain.repository.MemberRepository; +import roomescape.member.infrastructure.persistence.Member; +import roomescape.member.infrastructure.persistence.MemberRepository; +import roomescape.member.infrastructure.persistence.Role; import roomescape.payment.client.TossPaymentClient; import roomescape.payment.domain.CanceledPayment; import roomescape.payment.domain.Payment; @@ -124,12 +123,12 @@ public class ReservationControllerTest { @DisplayName("대기중인 예약을 취소한다.") void cancelWaiting() { // given - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); String accessTokenCookie = getAccessTokenCookieByLogin("email@email.com", "password"); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); - Member member1 = memberRepository.save(new Member("name1", "email1r@email.com", "password", Role.MEMBER)); + Member member1 = memberRepository.save(new Member(null, "name1", "email1r@email.com", "password", Role.MEMBER)); // when reservationRepository.save(new Reservation(LocalDate.now().plusDays(1), reservationTime, theme, member1, @@ -151,12 +150,14 @@ public class ReservationControllerTest { @DisplayName("회원은 자신이 아닌 다른 회원의 예약을 취소할 수 없다.") void cantCancelOtherMembersWaiting() { // given - Member confirmedMember = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); + Member confirmedMember = memberRepository.save( + new Member(null, "name", "email@email.com", "password", Role.MEMBER)); String accessTokenCookie = getAccessTokenCookieByLogin("email@email.com", "password"); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); - Member waitingMember = memberRepository.save(new Member("name1", "email1r@email.com", "password", Role.MEMBER)); + Member waitingMember = memberRepository.save( + new Member(null, "name1", "email1r@email.com", "password", Role.MEMBER)); // when reservationRepository.save(new Reservation(LocalDate.now().plusDays(1), reservationTime, theme, confirmedMember, @@ -182,7 +183,7 @@ public class ReservationControllerTest { ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); // when reservationRepository.save( @@ -206,7 +207,7 @@ public class ReservationControllerTest { @DisplayName("예약 취소는 관리자만 할 수 있다.") void canRemoveMyReservation() { // given - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); String accessTokenCookie = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword()); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); @@ -231,8 +232,10 @@ public class ReservationControllerTest { ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); - Member confirmedMember = memberRepository.save(new Member("name1", "email@email.com", "password", Role.MEMBER)); - Member waitingMember = memberRepository.save(new Member("name1", "email1@email.com", "password", Role.MEMBER)); + Member confirmedMember = memberRepository.save( + new Member(null, "name1", "email@email.com", "password", Role.MEMBER)); + Member waitingMember = memberRepository.save( + new Member(null, "name1", "email1@email.com", "password", Role.MEMBER)); reservationRepository.save( new Reservation(LocalDate.now(), reservationTime, theme, confirmedMember, ReservationStatus.CONFIRMED)); @@ -252,12 +255,13 @@ public class ReservationControllerTest { @DisplayName("본인의 예약이 아니더라도 관리자 권한이 있으면 예약 정보를 삭제할 수 있다.") void readReservationsSizeAfterPostAndDelete() { // given - Member member = memberRepository.save(new Member("name", "admin@admin.com", "password", Role.ADMIN)); + Member member = memberRepository.save(new Member(null, "name", "admin@admin.com", "password", Role.ADMIN)); String accessTokenCookie = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword()); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); - Member anotherMember = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); + Member anotherMember = memberRepository.save( + new Member(null, "name", "email@email.com", "password", Role.MEMBER)); Reservation reservation = reservationRepository.save( new Reservation(LocalDate.now(), reservationTime, theme, anotherMember, ReservationStatus.CONFIRMED)); @@ -341,7 +345,7 @@ public class ReservationControllerTest { ReservationTime time1 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(18, 30))); ReservationTime time2 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(19, 30))); - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.ADMIN)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.ADMIN)); String accessToken = getAccessTokenCookieByLogin("email@email.com", "password"); // when : 예약은 2개, 예약 대기는 1개 조회되어야 한다. @@ -371,7 +375,7 @@ public class ReservationControllerTest { // when Reservation saved = reservationRepository.save(new Reservation(date, time, theme, - memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)), + memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)), ReservationStatus.CONFIRMED_PAYMENT_REQUIRED)); // then @@ -391,7 +395,7 @@ public class ReservationControllerTest { LocalDate date = LocalDate.now().plusDays(1); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); ReservationTime time = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); Reservation saved = reservationRepository.save( new Reservation(date, time, theme, member, ReservationStatus.CONFIRMED)); @@ -421,7 +425,7 @@ public class ReservationControllerTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); String accessToken = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword()); // when : 이전 날짜의 예약을 추가하여 결제 승인 이후 DB 저장 과정에서 예외를 발생시킨다. @@ -514,8 +518,8 @@ public class ReservationControllerTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); - Member member1 = memberRepository.save(new Member("name1", "email1@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); + Member member1 = memberRepository.save(new Member(null, "name1", "email1@email.com", "password", Role.MEMBER)); String accessToken = getAccessTokenCookieByLogin(member.getEmail(), member.getPassword()); reservationRepository.save(new Reservation(date, time, theme, member1, ReservationStatus.CONFIRMED)); @@ -540,7 +544,7 @@ public class ReservationControllerTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); String accessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password"); Reservation waiting = reservationRepository.save( @@ -585,7 +589,7 @@ public class ReservationControllerTest { LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); String adminAccessToken = getAdminAccessTokenCookieByLogin("admin@email.com", "password"); @@ -601,7 +605,7 @@ public class ReservationControllerTest { } private String getAdminAccessTokenCookieByLogin(final String email, final String password) { - memberRepository.save(new Member("이름", email, password, Role.ADMIN)); + memberRepository.save(new Member(null, "이름", email, password, Role.ADMIN)); Map loginParams = Map.of( "email", email, diff --git a/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java b/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java index cefd9aff..9f809fe9 100644 --- a/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java +++ b/src/test/java/roomescape/reservation/controller/ReservationTimeControllerTest.java @@ -21,9 +21,9 @@ import org.springframework.test.context.jdbc.Sql.ExecutionPhase; import io.restassured.RestAssured; import io.restassured.http.ContentType; import io.restassured.http.Header; -import roomescape.member.domain.Member; -import roomescape.member.domain.Role; -import roomescape.member.domain.repository.MemberRepository; +import roomescape.member.infrastructure.persistence.Member; +import roomescape.member.infrastructure.persistence.MemberRepository; +import roomescape.member.infrastructure.persistence.Role; import roomescape.reservation.domain.Reservation; import roomescape.reservation.domain.ReservationStatus; import roomescape.reservation.domain.ReservationTime; @@ -199,7 +199,7 @@ public class ReservationTimeControllerTest { } private String getAdminAccessTokenCookieByLogin(String email, String password) { - memberRepository.save(new Member("이름", email, password, Role.ADMIN)); + memberRepository.save(new Member(null, "이름", email, password, Role.ADMIN)); Map loginParams = Map.of( "email", email, @@ -225,7 +225,7 @@ public class ReservationTimeControllerTest { ReservationTime reservationTime2 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(17, 30))); ReservationTime reservationTime3 = reservationTimeRepository.save(new ReservationTime(LocalTime.of(18, 30))); Theme theme = themeRepository.save(new Theme("테마명1", "설명", "썸네일URL")); - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); reservationRepository.save( new Reservation(today.plusDays(1), reservationTime1, theme, member, ReservationStatus.CONFIRMED)); diff --git a/src/test/java/roomescape/reservation/domain/ReservationTest.java b/src/test/java/roomescape/reservation/domain/ReservationTest.java index f66623e6..757b27a5 100644 --- a/src/test/java/roomescape/reservation/domain/ReservationTest.java +++ b/src/test/java/roomescape/reservation/domain/ReservationTest.java @@ -10,8 +10,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import roomescape.member.domain.Member; -import roomescape.member.domain.Role; +import roomescape.member.infrastructure.persistence.Member; +import roomescape.member.infrastructure.persistence.Role; import roomescape.system.exception.RoomEscapeException; import roomescape.theme.domain.Theme; @@ -34,17 +34,17 @@ public class ReservationTest { Arguments.of(null, new ReservationTime(LocalTime.now().plusHours(1)), new Theme("테마명", "설명", "썸네일URI"), - new Member("name", "email@email.com", "password", Role.MEMBER)), + new Member(null, "name", "email@email.com", "password", Role.MEMBER)), Arguments.of( LocalDate.now(), null, new Theme("테마명", "설명", "썸네일URI"), - new Member("name", "email@email.com", "password", Role.MEMBER)), + new Member(null, "name", "email@email.com", "password", Role.MEMBER)), Arguments.of( LocalDate.now(), new ReservationTime(LocalTime.now().plusHours(1)), null, - new Member("name", "email@email.com", "password", Role.MEMBER)), + new Member(null, "name", "email@email.com", "password", Role.MEMBER)), Arguments.of( LocalDate.now(), new ReservationTime(LocalTime.now().plusHours(1)), diff --git a/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java b/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java index 62cb372f..89dda95c 100644 --- a/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java +++ b/src/test/java/roomescape/reservation/domain/repository/ReservationSearchSpecificationTest.java @@ -13,9 +13,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.data.jpa.domain.Specification; -import roomescape.member.domain.Member; -import roomescape.member.domain.Role; -import roomescape.member.domain.repository.MemberRepository; +import roomescape.member.infrastructure.persistence.Member; +import roomescape.member.infrastructure.persistence.MemberRepository; +import roomescape.member.infrastructure.persistence.Role; import roomescape.reservation.domain.Reservation; import roomescape.reservation.domain.ReservationStatus; import roomescape.reservation.domain.ReservationTime; @@ -50,7 +50,7 @@ class ReservationSearchSpecificationTest { @BeforeEach void setUp() { LocalDateTime dateTime = LocalDateTime.now(); - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); ReservationTime time = timeRepository.save(new ReservationTime(dateTime.toLocalTime())); Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail")); diff --git a/src/test/java/roomescape/reservation/service/ReservationServiceTest.java b/src/test/java/roomescape/reservation/service/ReservationServiceTest.java index 59ecd913..17cacf6d 100644 --- a/src/test/java/roomescape/reservation/service/ReservationServiceTest.java +++ b/src/test/java/roomescape/reservation/service/ReservationServiceTest.java @@ -14,10 +14,10 @@ import org.springframework.context.annotation.Import; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql.ExecutionPhase; -import roomescape.member.domain.Member; -import roomescape.member.domain.Role; -import roomescape.member.domain.repository.MemberRepository; -import roomescape.member.service.MemberService; +import roomescape.member.business.MemberService; +import roomescape.member.infrastructure.persistence.Member; +import roomescape.member.infrastructure.persistence.MemberRepository; +import roomescape.member.infrastructure.persistence.Role; import roomescape.reservation.domain.Reservation; import roomescape.reservation.domain.ReservationStatus; import roomescape.reservation.domain.ReservationTime; @@ -53,8 +53,8 @@ class ReservationServiceTest { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); - Member member1 = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); - Member member2 = memberRepository.save(new Member("name2", "email2@email.com", "password", Role.MEMBER)); + Member member1 = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); + Member member2 = memberRepository.save(new Member(null, "name2", "email2@email.com", "password", Role.MEMBER)); LocalDate date = LocalDate.now().plusDays(1L); // when @@ -75,7 +75,7 @@ class ReservationServiceTest { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); LocalDate date = LocalDate.now().plusDays(1L); // when @@ -95,8 +95,8 @@ class ReservationServiceTest { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); - Member member1 = memberRepository.save(new Member("name1", "email1@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); + Member member1 = memberRepository.save(new Member(null, "name1", "email1@email.com", "password", Role.MEMBER)); LocalDate date = LocalDate.now().plusDays(1L); // when @@ -119,7 +119,7 @@ class ReservationServiceTest { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); LocalDate beforeDate = LocalDate.now().minusDays(1L); // when & then @@ -136,7 +136,7 @@ class ReservationServiceTest { LocalDateTime beforeTime = LocalDateTime.now().minusHours(1L).withNano(0); ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(beforeTime.toLocalTime())); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); // when & then assertThatThrownBy(() -> reservationService.addReservation( @@ -180,9 +180,9 @@ class ReservationServiceTest { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); - Member admin = memberRepository.save(new Member("admin", "admin@email.com", "password", Role.ADMIN)); - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); - Member member1 = memberRepository.save(new Member("name1", "email1@email.com", "password", Role.MEMBER)); + Member admin = memberRepository.save(new Member(null, "admin", "admin@email.com", "password", Role.ADMIN)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); + Member member1 = memberRepository.save(new Member(null, "name1", "email1@email.com", "password", Role.MEMBER)); reservationService.addReservation( new ReservationRequest(LocalDate.now().plusDays(1L), reservationTime.getId(), theme.getId(), @@ -203,8 +203,8 @@ class ReservationServiceTest { // given ReservationTime reservationTime = reservationTimeRepository.save(new ReservationTime(LocalTime.of(12, 30))); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); - Member admin = memberRepository.save(new Member("admin", "admin@email.com", "password", Role.ADMIN)); - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); + Member admin = memberRepository.save(new Member(null, "admin", "admin@email.com", "password", Role.ADMIN)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); // when ReservationResponse waiting = reservationService.addWaiting( diff --git a/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java b/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java index 5b3e57a9..73682f55 100644 --- a/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java +++ b/src/test/java/roomescape/reservation/service/ReservationTimeServiceTest.java @@ -13,9 +13,9 @@ import org.springframework.context.annotation.Import; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql.ExecutionPhase; -import roomescape.member.domain.Member; -import roomescape.member.domain.Role; -import roomescape.member.domain.repository.MemberRepository; +import roomescape.member.infrastructure.persistence.Member; +import roomescape.member.infrastructure.persistence.MemberRepository; +import roomescape.member.infrastructure.persistence.Role; import roomescape.reservation.domain.Reservation; import roomescape.reservation.domain.ReservationStatus; import roomescape.reservation.domain.ReservationTime; @@ -75,7 +75,7 @@ class ReservationTimeServiceTest { ReservationTime reservationTime = reservationTimeRepository.save( new ReservationTime(localDateTime.toLocalTime())); Theme theme = themeRepository.save(new Theme("테마명", "설명", "썸네일URL")); - Member member = memberRepository.save(new Member("name", "email@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "name", "email@email.com", "password", Role.MEMBER)); // when reservationRepository.save(new Reservation(localDateTime.toLocalDate(), reservationTime, theme, member, diff --git a/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java b/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java index 973ccf03..fe0bec73 100644 --- a/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java +++ b/src/test/java/roomescape/reservation/service/ReservationWithPaymentServiceTest.java @@ -13,9 +13,9 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.jdbc.Sql.ExecutionPhase; -import roomescape.member.domain.Member; -import roomescape.member.domain.Role; -import roomescape.member.domain.repository.MemberRepository; +import roomescape.member.infrastructure.persistence.Member; +import roomescape.member.infrastructure.persistence.MemberRepository; +import roomescape.member.infrastructure.persistence.Role; import roomescape.payment.domain.repository.CanceledPaymentRepository; import roomescape.payment.domain.repository.PaymentRepository; import roomescape.payment.dto.request.PaymentCancelRequest; @@ -57,7 +57,7 @@ class ReservationWithPaymentServiceTest { LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0); LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); - Member member = memberRepository.save(new Member("member", "email@email.com", "password", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "member", "email@email.com", "password", Role.MEMBER)); Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); ReservationRequest reservationRequest = new ReservationRequest(date, time.getId(), theme.getId(), "payment-key", "order-id", 10000L, "NORMAL"); @@ -92,7 +92,7 @@ class ReservationWithPaymentServiceTest { LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0); LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); - Member member = memberRepository.save(new Member("member", "admin@email.com", "password", Role.ADMIN)); + Member member = memberRepository.save(new Member(null, "member", "admin@email.com", "password", Role.ADMIN)); Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); ReservationRequest reservationRequest = new ReservationRequest(date, time.getId(), theme.getId(), "payment-key", "order-id", 10000L, "NORMAL"); @@ -119,7 +119,7 @@ class ReservationWithPaymentServiceTest { LocalDateTime localDateTime = LocalDateTime.now().plusHours(1L); LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); - Member member = memberRepository.save(new Member("member", "admin@email.com", "password", Role.ADMIN)); + Member member = memberRepository.save(new Member(null, "member", "admin@email.com", "password", Role.ADMIN)); Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); Reservation saved = reservationRepository.save( @@ -140,7 +140,7 @@ class ReservationWithPaymentServiceTest { LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0); LocalDate date = localDateTime.toLocalDate(); ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime())); - Member member = memberRepository.save(new Member("member", "admin@email.com", "password", Role.ADMIN)); + Member member = memberRepository.save(new Member(null, "member", "admin@email.com", "password", Role.ADMIN)); Theme theme = themeRepository.save(new Theme("name", "desc", "thumbnail")); ReservationRequest reservationRequest = new ReservationRequest(date, time.getId(), theme.getId(), "payment-key", "order-id", 10000L, "NORMAL"); diff --git a/src/test/java/roomescape/system/auth/business/AuthServiceTest.kt b/src/test/java/roomescape/system/auth/business/AuthServiceTest.kt new file mode 100644 index 00000000..8e415266 --- /dev/null +++ b/src/test/java/roomescape/system/auth/business/AuthServiceTest.kt @@ -0,0 +1,83 @@ +package roomescape.system.auth.business + +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import org.springframework.data.repository.findByIdOrNull +import roomescape.common.JwtFixture +import roomescape.common.MemberFixture +import roomescape.member.business.MemberService +import roomescape.member.infrastructure.persistence.Member +import roomescape.member.infrastructure.persistence.MemberRepository +import roomescape.system.auth.infrastructure.jwt.JwtHandler +import roomescape.system.auth.service.AuthService +import roomescape.system.exception.ErrorType +import roomescape.system.exception.RoomEscapeException + + +class AuthServiceTest : BehaviorSpec({ + val memberRepository: MemberRepository = mockk() + val memberService: MemberService = MemberService(memberRepository) + val jwtHandler: JwtHandler = JwtFixture.create() + + val authService = AuthService(memberService, jwtHandler) + val user: Member = MemberFixture.user() + + Given("로그인 요청을 받으면") { + When("이메일과 비밀번호로 회원을 찾고") { + val request = MemberFixture.userLoginRequest() + + Then("회원이 있다면 JWT 토큰을 생성한 뒤 반환한다.") { + every { + memberRepository.findByEmailAndPassword(request.email, request.password) + } returns user + + val accessToken: String = authService.login(request).accessToken + + accessToken.isNotBlank() shouldBe true + jwtHandler.getMemberIdFromToken(accessToken) shouldBe user.id + } + + Then("회원이 없다면 예외를 던진다.") { + every { + memberRepository.findByEmailAndPassword(request.email, request.password) + } returns null + + val exception = shouldThrow { + authService.login(request) + } + + exception.errorType shouldBe ErrorType.MEMBER_NOT_FOUND + } + } + } + + Given("로그인 확인 요청을 받으면") { + When("회원 ID로 회원을 찾고") { + val userId: Long = user.id!! + + Then("회원이 있다면 회원의 이름을 반환한다.") { + every { memberRepository.findByIdOrNull(userId) } returns user + + val response = authService.checkLogin(userId) + + assertSoftly(response) { + this.name shouldBe user.name + } + } + + Then("회원이 없다면 예외를 던진다.") { + every { memberRepository.findByIdOrNull(userId) } returns null + + val exception = shouldThrow { + authService.checkLogin(userId) + } + + exception.errorType shouldBe ErrorType.MEMBER_NOT_FOUND + } + } + } +}) diff --git a/src/test/java/roomescape/system/auth/controller/AuthControllerTest.java b/src/test/java/roomescape/system/auth/controller/AuthControllerTest.java deleted file mode 100644 index fb3918e3..00000000 --- a/src/test/java/roomescape/system/auth/controller/AuthControllerTest.java +++ /dev/null @@ -1,118 +0,0 @@ -package roomescape.system.auth.controller; - -import static org.assertj.core.api.Assertions.*; -import static org.hamcrest.Matchers.*; - -import java.util.Map; - -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.web.server.LocalServerPort; -import org.springframework.test.context.jdbc.Sql; - -import io.restassured.RestAssured; -import io.restassured.http.ContentType; -import roomescape.member.domain.Member; -import roomescape.member.domain.Role; -import roomescape.member.domain.repository.MemberRepository; - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Sql(scripts = "/truncate.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) -class AuthControllerTest { - - @Autowired - private MemberRepository memberRepository; - - @LocalServerPort - private int port; - - @Test - @DisplayName("로그인에 성공하면 JWT accessToken을 응답 받는다.") - void getJwtAccessTokenWhenlogin() { - // given - String email = "test@email.com"; - String password = "12341234"; - memberRepository.save(new Member("이름", email, password, Role.MEMBER)); - - Map loginParams = Map.of( - "email", email, - "password", password - ); - - // when - Map cookies = RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .body(loginParams) - .when().post("/login") - .then().log().all().extract().cookies(); - - // then - assertThat(cookies.get("accessToken")).isNotNull(); - } - - @Test - @DisplayName("로그인 검증 시, 회원의 name을 응답 받는다.") - void checkLogin() { - // given - String email = "test@test.com"; - String password = "12341234"; - String accessTokenCookie = getAccessTokenCookieByLogin(email, password); - - // when & then - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .header("cookie", accessTokenCookie) - .when().get("/login/check") - .then() - .body("data.name", is("이름")); - } - - @Test - @DisplayName("로그인 없이 검증요청을 보내면 401 Unauthorized 를 응답한다.") - void checkLoginFailByNotAuthorized() { - RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .when().get("/login/check") - .then() - .statusCode(401); - } - - @Test - @DisplayName("로그아웃 요청 시, accessToken 쿠키가 삭제된다.") - void checkLogout() { - // given - String accessToken = getAccessTokenCookieByLogin("email@email.com", "password"); - - // when & then - RestAssured.given().log().all() - .port(port) - .header("cookie", accessToken) - .when().post("/logout") - .then() - .statusCode(200) - .cookie("accessToken", ""); - } - - private String getAccessTokenCookieByLogin(final String email, final String password) { - memberRepository.save(new Member("이름", email, password, Role.ADMIN)); - - Map loginParams = Map.of( - "email", email, - "password", password - ); - - String accessToken = RestAssured.given().log().all() - .contentType(ContentType.JSON) - .port(port) - .body(loginParams) - .when().post("/login") - .then().log().all().extract().cookie("accessToken"); - - return "accessToken=" + accessToken; - } -} diff --git a/src/test/java/roomescape/system/auth/infrastructure/jwt/JwtHandlerTest.kt b/src/test/java/roomescape/system/auth/infrastructure/jwt/JwtHandlerTest.kt new file mode 100644 index 00000000..b7565255 --- /dev/null +++ b/src/test/java/roomescape/system/auth/infrastructure/jwt/JwtHandlerTest.kt @@ -0,0 +1,61 @@ +package roomescape.system.auth.infrastructure.jwt + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.SignatureAlgorithm +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import roomescape.common.JwtFixture +import roomescape.system.exception.ErrorType +import roomescape.system.exception.RoomEscapeException +import java.util.* +import kotlin.random.Random + +class JwtHandlerTest : FunSpec({ + + context("JWT 토큰 조회") { + val memberId = Random.nextLong() + val jwtHandler: JwtHandler = JwtFixture.create() + + test("토큰에서 멤버 ID를 올바르게 추출한다.") { + val token = jwtHandler.createToken(memberId) + val extractedMemberId = jwtHandler.getMemberIdFromToken(token) + + extractedMemberId shouldBe memberId + } + + test("만료된 토큰이면 예외를 던진다.") { + // given + val expirationTime = 0L + val shortExpirationTimeJwtHandler: JwtHandler = JwtFixture.create(expirationTime = expirationTime) + val token = shortExpirationTimeJwtHandler.createToken(memberId) + + Thread.sleep(expirationTime) // 만료 시간 이후로 대기 + + // when & then + shouldThrow { + shortExpirationTimeJwtHandler.getMemberIdFromToken(token) + }.errorType shouldBe ErrorType.EXPIRED_TOKEN + } + + test("토큰이 빈 값이면 예외를 던진다.") { + shouldThrow { + jwtHandler.getMemberIdFromToken("") + }.errorType shouldBe ErrorType.INVALID_TOKEN + } + + test("시크릿 키가 잘못된 경우 예외를 던진다.") { + val now: Date = Date() + val invalidSignatureToken: String = Jwts.builder() + .claim("memberId", memberId) + .setIssuedAt(now) + .setExpiration(Date(now.time + JwtFixture.EXPIRATION_TIME)) + .signWith(SignatureAlgorithm.HS256, JwtFixture.SECRET_KEY.substring(1).toByteArray()) + .compact() + + shouldThrow { + jwtHandler.getMemberIdFromToken(invalidSignatureToken) + }.errorType shouldBe ErrorType.INVALID_SIGNATURE_TOKEN + } + } +}) diff --git a/src/test/java/roomescape/system/auth/service/AuthServiceTest.java b/src/test/java/roomescape/system/auth/service/AuthServiceTest.java deleted file mode 100644 index bf8a5af8..00000000 --- a/src/test/java/roomescape/system/auth/service/AuthServiceTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package roomescape.system.auth.service; - -import static org.assertj.core.api.Assertions.*; - -import org.assertj.core.api.Assertions; -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.context.annotation.Import; - -import roomescape.member.domain.Member; -import roomescape.member.domain.Role; -import roomescape.member.domain.repository.MemberRepository; -import roomescape.member.service.MemberService; -import roomescape.system.auth.dto.LoginRequest; -import roomescape.system.auth.jwt.JwtHandler; -import roomescape.system.auth.jwt.dto.TokenDto; -import roomescape.system.exception.RoomEscapeException; - -@SpringBootTest -@Import({AuthService.class, JwtHandler.class, MemberService.class}) -class AuthServiceTest { - - @Autowired - private AuthService authService; - @Autowired - private MemberRepository memberRepository; - - @Test - @DisplayName("로그인 성공시 JWT accessToken 을 반환한다.") - void loginSuccess() { - // given - Member member = memberRepository.save(new Member("이름", "test@test.com", "12341234", Role.MEMBER)); - - // when - TokenDto response = authService.login(new LoginRequest(member.getEmail(), member.getPassword())); - - // then - assertThat(response.accessToken()).isNotNull(); - } - - @Test - @DisplayName("존재하지 않는 회원 email 또는 password로 로그인하면 예외가 발생한다.") - void loginFailByNotExistMemberInfo() { - // given - String notExistEmail = "invalid@test.com"; - String notExistPassword = "invalid1234"; - - // when & then - Assertions.assertThatThrownBy(() -> authService.login(new LoginRequest(notExistEmail, notExistPassword))) - .isInstanceOf(RoomEscapeException.class); - } - - @Test - @DisplayName("존재하지 않는 회원의 memberId로 로그인 여부를 체크하면 예외가 발생한다.") - void checkLoginFailByNotExistMemberInfo() { - // given - Long notExistMemberId = (long)(memberRepository.findAll().size() + 1); - - // when & then - Assertions.assertThatThrownBy(() -> authService.checkLogin(notExistMemberId)) - .isInstanceOf(RoomEscapeException.class); - } -} diff --git a/src/test/java/roomescape/system/auth/web/AuthControllerTest.kt b/src/test/java/roomescape/system/auth/web/AuthControllerTest.kt new file mode 100644 index 00000000..7e6303c9 --- /dev/null +++ b/src/test/java/roomescape/system/auth/web/AuthControllerTest.kt @@ -0,0 +1,139 @@ +package roomescape.system.auth.web + +import io.mockk.every +import org.hamcrest.Matchers.containsString +import org.hamcrest.Matchers.`is` +import org.springframework.data.repository.findByIdOrNull +import roomescape.common.MemberFixture +import roomescape.common.RoomescapeApiTest +import roomescape.system.exception.ErrorType + +class AuthControllerTest : RoomescapeApiTest() { + + val userRequest: LoginRequest = MemberFixture.userLoginRequest() + + init { + Given("로그인 요청을 보낼 때") { + val endpoint: String = "/login" + + When("로그인에 성공하면") { + val expectedToken: String = "expectedToken" + + every { + memberRepository.findByEmailAndPassword(userRequest.email, userRequest.password) + } returns user + + every { + jwtHandler.createToken(user.id!!) + } returns expectedToken + + Then("토큰을 쿠키에 담아 응답한다") { + runPostTest(endpoint, body = MemberFixture.userLoginRequest()) { + statusCode(200) + cookie("accessToken", expectedToken) + header("Set-Cookie", containsString("Max-Age=1800000")) + header("Set-Cookie", containsString("HttpOnly")) + header("Set-Cookie", containsString("Secure")) + } + } + } + + When("회원을 찾지 못하면") { + every { + memberRepository.findByEmailAndPassword(userRequest.email, userRequest.password) + } returns null + + Then("400 에러를 응답한다") { + runPostTest(endpoint, body = userRequest) { + log().all() + statusCode(400) + body("errorType", `is`(ErrorType.MEMBER_NOT_FOUND.name)) + } + } + } + + When("잘못된 요청을 보내면 400 에러를 응답한다.") { + + Then("이메일 형식이 잘못된 경우") { + val invalidRequest: LoginRequest = userRequest.copy(email = "invalid") + + runPostTest(endpoint, body = invalidRequest) { + log().all() + statusCode(400) + body("message", `is`("이메일 형식이 일치하지 않습니다. 예시: abc123@gmail.com")) + } + } + + Then("비밀번호가 공백인 경우") { + val invalidRequest = userRequest.copy(password = " ") + + runPostTest(endpoint, body = invalidRequest) { + log().all() + statusCode(400) + body("message", `is`("비밀번호는 공백일 수 없습니다.")) + } + } + } + } + + Given("로그인 상태를 확인할 때") { + val endpoint: String = "/login/check" + + When("로그인된 회원의 ID로 요청하면") { + every { jwtHandler.getMemberIdFromToken(any()) } returns user.id!! + every { memberRepository.findByIdOrNull(user.id!!) } returns user + + Then("회원의 이름을 응답한다") { + runGetTest(endpoint) { + statusCode(200) + body("data.name", `is`(user.name)) + } + } + } + + When("토큰은 있지만 회원을 찾을 수 없으면") { + val invalidMemberId: Long = -1L + + every { jwtHandler.getMemberIdFromToken(any()) } returns invalidMemberId + every { memberRepository.findByIdOrNull(invalidMemberId) } returns null + + Then("400 에러를 응답한다.") { + runGetTest(endpoint) { + statusCode(400) + body("errorType", `is`(ErrorType.MEMBER_NOT_FOUND.name)) + } + } + } + } + + Given("로그아웃 요청을 보낼 때") { + val endpoint: String = "/logout" + + When("로그인 상태가 아니라면") { + setUpNotLoggedIn() + + Then("로그인 페이지로 이동한다.") { + runPostTest(endpoint) { + log().all() + statusCode(302) + header("Location", containsString("/login")) + } + } + } + + When("로그인 상태라면") { + setUpUser() + every { memberRepository.findByIdOrNull(user.id!!) } returns user + + Then("토큰의 존재 여부와 무관하게 토큰을 만료시킨다.") { + runPostTest(endpoint) { + log().all() + statusCode(200) + cookie("accessToken", "") + header("Set-Cookie", containsString("Max-Age=0")) + } + } + } + } + } +} diff --git a/src/test/java/roomescape/theme/controller/ThemeControllerTest.java b/src/test/java/roomescape/theme/controller/ThemeControllerTest.java index 8eba3ab3..391ed044 100644 --- a/src/test/java/roomescape/theme/controller/ThemeControllerTest.java +++ b/src/test/java/roomescape/theme/controller/ThemeControllerTest.java @@ -18,9 +18,9 @@ import org.springframework.test.context.jdbc.Sql.ExecutionPhase; import io.restassured.RestAssured; import io.restassured.http.ContentType; import io.restassured.http.Header; -import roomescape.member.domain.Member; -import roomescape.member.domain.Role; -import roomescape.member.domain.repository.MemberRepository; +import roomescape.member.infrastructure.persistence.Member; +import roomescape.member.infrastructure.persistence.MemberRepository; +import roomescape.member.infrastructure.persistence.Role; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Sql(scripts = "/truncate.sql", executionPhase = ExecutionPhase.BEFORE_TEST_METHOD) @@ -165,7 +165,7 @@ class ThemeControllerTest { } private String getAdminAccessTokenCookieByLogin(final String email, final String password) { - memberRepository.save(new Member("이름", email, password, Role.ADMIN)); + memberRepository.save(new Member(null, "이름", email, password, Role.ADMIN)); Map loginParams = Map.of( "email", email, diff --git a/src/test/java/roomescape/theme/service/ThemeServiceTest.java b/src/test/java/roomescape/theme/service/ThemeServiceTest.java index be91c3ca..3ef8b8b7 100644 --- a/src/test/java/roomescape/theme/service/ThemeServiceTest.java +++ b/src/test/java/roomescape/theme/service/ThemeServiceTest.java @@ -13,10 +13,10 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; import org.springframework.test.context.jdbc.Sql; -import roomescape.member.domain.Member; -import roomescape.member.domain.Role; -import roomescape.member.domain.repository.MemberRepository; -import roomescape.member.service.MemberService; +import roomescape.member.business.MemberService; +import roomescape.member.infrastructure.persistence.Member; +import roomescape.member.infrastructure.persistence.MemberRepository; +import roomescape.member.infrastructure.persistence.Role; import roomescape.reservation.dto.request.ReservationRequest; import roomescape.reservation.dto.request.ReservationTimeRequest; import roomescape.reservation.dto.response.ReservationTimeResponse; @@ -152,7 +152,7 @@ class ThemeServiceTest { ReservationTimeResponse time = reservationTimeService.addTime( new ReservationTimeRequest(dateTime.toLocalTime())); Theme theme = themeRepository.save(new Theme("name", "description", "thumbnail")); - Member member = memberRepository.save(new Member("member", "password", "name", Role.MEMBER)); + Member member = memberRepository.save(new Member(null, "member", "password", "name", Role.MEMBER)); reservationService.addReservation( new ReservationRequest(dateTime.toLocalDate(), time.id(), theme.getId(), "paymentKey", "orderId", 1000L, "NORMAL"), member.getId()); diff --git a/src/test/java/roomescape/view/controller/PageControllerTest.kt b/src/test/java/roomescape/view/controller/PageControllerTest.kt index 170b397c..825467b5 100644 --- a/src/test/java/roomescape/view/controller/PageControllerTest.kt +++ b/src/test/java/roomescape/view/controller/PageControllerTest.kt @@ -1,66 +1,34 @@ package roomescape.view.controller -import com.ninjasquad.springmockk.MockkBean -import io.kotest.core.spec.style.BehaviorSpec -import io.mockk.every -import io.restassured.module.kotlin.extensions.Given -import io.restassured.module.kotlin.extensions.Then -import io.restassured.module.kotlin.extensions.When -import io.restassured.response.ValidatableResponse import org.hamcrest.Matchers.containsString -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.web.server.LocalServerPort -import org.springframework.http.HttpStatus -import roomescape.common.MemberFixture -import roomescape.common.NoSqlInitialize -import roomescape.member.domain.Member -import roomescape.member.service.MemberService -import roomescape.system.auth.jwt.JwtHandler -import roomescape.system.exception.ErrorType -import roomescape.system.exception.RoomEscapeException - -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@NoSqlInitialize -class PageControllerTest( - @LocalServerPort val port: Int, -) : BehaviorSpec() { - - @MockkBean - private lateinit var jwtHandler: JwtHandler - - @MockkBean - private lateinit var memberService: MemberService - - private val admin: Member = MemberFixture.admin() - private val user: Member = MemberFixture.user() +import roomescape.common.RoomescapeApiTest +class PageControllerTest() : RoomescapeApiTest() { init { listOf("/", "/login").forEach { given("GET $it 요청은") { `when`("로그인 및 권한 여부와 관계없이 성공한다.") { then("비회원") { - runTest(it) { + setUpNotLoggedIn() + + runGetTest(it) { statusCode(200) } } then("회원") { - every { - jwtHandler.getMemberIdFromToken(any()) - } returns user.id + setUpUser() - runTest(it) { + runGetTest(it) { statusCode(200) } } then("관리자") { - every { - jwtHandler.getMemberIdFromToken(any()) - } returns admin.id + setUpAdmin() - runTest(it) { + runGetTest(it) { statusCode(200) } } @@ -71,24 +39,20 @@ class PageControllerTest( listOf("/admin", "/admin/reservation", "/admin/time", "/admin/theme", "/admin/waiting").forEach { given("GET $it 요청을") { `when`("관리자가 보내면") { - every { - jwtHandler.getMemberIdFromToken(any()) - } returns admin.id + setUpAdmin() then("성공한다.") { - runTest(it) { + runGetTest(it) { statusCode(200) } } } `when`("회원이 보내면") { - every { - jwtHandler.getMemberIdFromToken(any()) - } returns user.id + setUpUser() then("로그인 페이지로 이동한다.") { - runTest(it) { + runGetTest(it) { statusCode(200) body(containsString("Login")) } @@ -101,20 +65,16 @@ class PageControllerTest( given("GET $it 요청을") { `when`("로그인 된 회원이 보내면 성공한다.") { then("회원") { - every { - jwtHandler.getMemberIdFromToken(any()) - } returns user.id + setUpUser() - runTest(it) { + runGetTest(it) { statusCode(200) } } then("관리자") { - every { - jwtHandler.getMemberIdFromToken(any()) - } returns admin.id + setUpAdmin() - runTest(it) { + runGetTest(it) { statusCode(200) } } @@ -122,11 +82,9 @@ class PageControllerTest( `when`("로그인 없이 보내면") { then("로그인 페이지로 이동한다.") { - every { - jwtHandler.getMemberIdFromToken(any()) - } returns null + setUpNotLoggedIn() - runTest(it) { + runGetTest(it) { statusCode(200) body(containsString("Login")) } @@ -135,27 +93,4 @@ class PageControllerTest( } } } - - fun runTest(endpoint: String, assert: ValidatableResponse.() -> Unit) { - setUpMocks() - - Given { - port(port) - header("Cookie", "accessToken=token") - } When { - get(endpoint) - } Then assert - } - - private fun setUpMocks() { - every { memberService.findMemberById(admin.id) } returns admin - - every { memberService.findMemberById(user.id) } returns user - - every { memberService.findMemberById(null) } throws RoomEscapeException( - ErrorType.MEMBER_NOT_FOUND, - String.format("[memberId: %d]", null), - HttpStatus.BAD_REQUEST - ) - } } \ No newline at end of file