diff --git a/Dockerfile b/Dockerfile index c57f141d..4f3db3cd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,7 @@ FROM eclipse-temurin:17-jdk-alpine + +# Redis 설치 +RUN apk add --no-cache redis + COPY ./build/libs/*SNAPSHOT.jar project.jar -ENTRYPOINT ["java", "-jar", "project.jar"] \ No newline at end of file +ENTRYPOINT redis-server & java -jar project.jar \ No newline at end of file diff --git a/build.gradle b/build.gradle index 2e3c9d44..ebf2d2d8 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' // Lombok compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/com/example/mate/common/config/SecurityConfig.java b/src/main/java/com/example/mate/common/config/SecurityConfig.java index 0d296f01..507ef6c8 100644 --- a/src/main/java/com/example/mate/common/config/SecurityConfig.java +++ b/src/main/java/com/example/mate/common/config/SecurityConfig.java @@ -1,7 +1,5 @@ package com.example.mate.common.config; -//import com.example.mate.common.security.filter.JwtCheckFilter; - import com.example.mate.common.security.filter.JwtCheckFilter; import java.util.List; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/example/mate/common/config/SwaggerConfig.java b/src/main/java/com/example/mate/common/config/SwaggerConfig.java index 862b8e76..b761482f 100644 --- a/src/main/java/com/example/mate/common/config/SwaggerConfig.java +++ b/src/main/java/com/example/mate/common/config/SwaggerConfig.java @@ -5,12 +5,9 @@ import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; -import io.swagger.v3.oas.models.servers.Server; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import java.util.List; - @Configuration public class SwaggerConfig { @Bean @@ -22,7 +19,6 @@ public OpenAPI openAPI() { return new OpenAPI() .info(info) - .servers(List.of(new Server().url("http://localhost:8080"))) .addSecurityItem(new SecurityRequirement().addList("Bearer Authentication")) .components(new Components().addSecuritySchemes("Bearer Authentication", createAPIKeyScheme())); } diff --git a/src/main/java/com/example/mate/common/config/WebMvcConfig.java b/src/main/java/com/example/mate/common/config/WebMvcConfig.java new file mode 100644 index 00000000..49532ab8 --- /dev/null +++ b/src/main/java/com/example/mate/common/config/WebMvcConfig.java @@ -0,0 +1,23 @@ +package com.example.mate.common.config; + +import com.example.mate.common.validator.ValidPageableArgumentResolver; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + private final ValidPageableArgumentResolver validPageableArgumentResolver; + + public WebMvcConfig(ValidPageableArgumentResolver validPageableArgumentResolver) { + this.validPageableArgumentResolver = validPageableArgumentResolver; + } + + @Override + public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { + resolvers.add(validPageableArgumentResolver); + } +} diff --git a/src/main/java/com/example/mate/common/error/ErrorCode.java b/src/main/java/com/example/mate/common/error/ErrorCode.java index dad30a4e..86a3de47 100644 --- a/src/main/java/com/example/mate/common/error/ErrorCode.java +++ b/src/main/java/com/example/mate/common/error/ErrorCode.java @@ -15,6 +15,7 @@ public enum ErrorCode { AUTH_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "A002", "인증에 실패했습니다. 유효한 토큰을 제공해주세요."), AUTH_FORBIDDEN(HttpStatus.FORBIDDEN, "A003", "접근 권한이 없습니다. 권한을 확인해주세요."), UNAUTHORIZED_USER(HttpStatus.UNAUTHORIZED, "A004", "인증되지 않은 사용자입니다"), + INVALID_AUTH_TOKEN(HttpStatus.BAD_REQUEST, "A005", "잘못된 토큰 형식입니다."), // Team TEAM_NOT_FOUND(HttpStatus.NOT_FOUND, "T001", "팀을 찾을 수 없습니다"), diff --git a/src/main/java/com/example/mate/common/response/PageResponse.java b/src/main/java/com/example/mate/common/response/PageResponse.java index caa4251d..0ab1e521 100644 --- a/src/main/java/com/example/mate/common/response/PageResponse.java +++ b/src/main/java/com/example/mate/common/response/PageResponse.java @@ -6,8 +6,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; @Getter @Builder @@ -41,13 +39,21 @@ public static <R, T> PageResponse<T> from(Page<R> page, List<T> content) { .build(); } - // Pageable 검증 메서드 - public static Pageable validatePageable(Pageable pageable) { - // pageNumber 검증: 0보다 작은 값은 0으로 처리 - int pageNumber = Math.max(pageable.getPageNumber(), 0); - - // pageSize 검증: 0 이하이면 기본값 10으로 설정 - int pageSize = pageable.getPageSize() <= 0 ? 10 : pageable.getPageSize(); - return PageRequest.of(pageNumber, pageSize, pageable.getSort()); + /** + * Page 객체를 기반으로 PageResponse 를 생성하는 팩토리 메서드 + * + * @param page Spring Data JPA 의 Page 객체 + * @param <T> 변환된 데이터 타입 + * @return PageResponse + */ + public static <T> PageResponse<T> from(Page<T> page) { + return PageResponse.<T>builder() + .content(page.getContent()) + .totalPages(page.getTotalPages()) + .totalElements(page.getTotalElements()) + .hasNext(page.hasNext()) + .pageNumber(page.getNumber()) + .pageSize(page.getSize()) + .build(); } } \ No newline at end of file diff --git a/src/main/java/com/example/mate/common/security/filter/JwtCheckFilter.java b/src/main/java/com/example/mate/common/security/filter/JwtCheckFilter.java index 381c1724..83f394bf 100644 --- a/src/main/java/com/example/mate/common/security/filter/JwtCheckFilter.java +++ b/src/main/java/com/example/mate/common/security/filter/JwtCheckFilter.java @@ -2,13 +2,10 @@ import com.example.mate.common.security.auth.AuthMember; import com.example.mate.common.security.util.JwtUtil; +import com.example.mate.domain.member.service.LogoutRedisService; import jakarta.servlet.FilterChain; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.Arrays; -import java.util.Map; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -16,11 +13,17 @@ import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + @Component @RequiredArgsConstructor public class JwtCheckFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; + private final LogoutRedisService logoutRedisService; // 필터링 적용하지 않을 URI 체크 @Override @@ -44,8 +47,15 @@ protected void doFilterInternal(HttpServletRequest request, return; } - // 토큰 유효성 검증 후 SecurityContext 설정 String accessToken = headerAuth.substring(7); // "Bearer " 제외한 토큰 저장 + + // 액세스 토큰이 블랙리스트에 있는지 확인 + if (logoutRedisService.isTokenBlacklisted(accessToken)) { + handleException(response, new Exception("ACCESS TOKEN IS BLACKLISTED")); + return; + } + + // 액세스 토큰의 모든 유효성 검증 후 SecurityContext 설정 try { Map<String, Object> claims = jwtUtil.validateToken(accessToken); setAuthentication(claims); // 인증 정보 설정 @@ -62,6 +72,7 @@ private boolean isExcludedPath(String requestURI) { requestURI.startsWith("/api/auth") || requestURI.startsWith("/api/members/join") || requestURI.startsWith("/swagger-ui") || + requestURI.startsWith("/v3/api-docs") || requestURI.startsWith("/api/members/login"); } diff --git a/src/main/java/com/example/mate/common/utils/validator/EnumValidator.java b/src/main/java/com/example/mate/common/validator/EnumValidator.java similarity index 94% rename from src/main/java/com/example/mate/common/utils/validator/EnumValidator.java rename to src/main/java/com/example/mate/common/validator/EnumValidator.java index 360f1b64..c526b6b6 100644 --- a/src/main/java/com/example/mate/common/utils/validator/EnumValidator.java +++ b/src/main/java/com/example/mate/common/validator/EnumValidator.java @@ -1,4 +1,4 @@ -package com.example.mate.common.utils.validator; +package com.example.mate.common.validator; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; diff --git a/src/main/java/com/example/mate/common/utils/validator/ValidEnum.java b/src/main/java/com/example/mate/common/validator/ValidEnum.java similarity index 93% rename from src/main/java/com/example/mate/common/utils/validator/ValidEnum.java rename to src/main/java/com/example/mate/common/validator/ValidEnum.java index 91f92c65..f713fa74 100644 --- a/src/main/java/com/example/mate/common/utils/validator/ValidEnum.java +++ b/src/main/java/com/example/mate/common/validator/ValidEnum.java @@ -1,4 +1,4 @@ -package com.example.mate.common.utils.validator; +package com.example.mate.common.validator; import jakarta.validation.Constraint; import jakarta.validation.Payload; diff --git a/src/main/java/com/example/mate/common/validator/ValidPageable.java b/src/main/java/com/example/mate/common/validator/ValidPageable.java new file mode 100644 index 00000000..1fc1391e --- /dev/null +++ b/src/main/java/com/example/mate/common/validator/ValidPageable.java @@ -0,0 +1,27 @@ +package com.example.mate.common.validator; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; +import org.springframework.data.domain.Sort.Direction; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER}) +public @interface ValidPageable { + + @AliasFor("size") + int value() default 10; + + @AliasFor("value") + int size() default 10; + + int page() default 0; + + String[] sort() default {}; + + Direction direction() default Direction.ASC; +} diff --git a/src/main/java/com/example/mate/common/validator/ValidPageableArgumentResolver.java b/src/main/java/com/example/mate/common/validator/ValidPageableArgumentResolver.java new file mode 100644 index 00000000..6ad566cb --- /dev/null +++ b/src/main/java/com/example/mate/common/validator/ValidPageableArgumentResolver.java @@ -0,0 +1,37 @@ +package com.example.mate.common.validator; + +import org.springframework.core.MethodParameter; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +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.ModelAndViewContainer; + +/** + * 커스텀 Pageable 리졸버로, {@code @ValidPageable} 어노테이션이 붙은 Pageable 매개변수의 유효성 검사를 수행합니다. + */ +@Component +public class ValidPageableArgumentResolver extends PageableHandlerMethodArgumentResolver { + + @Override + public Pageable resolveArgument(MethodParameter methodParameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) { + + // 기본 Pageable 생성 + Pageable pageable = super.resolveArgument(methodParameter, mavContainer, webRequest, binderFactory); + + // @ValidPageable 어노테이션 확인 + ValidPageable validPageable = methodParameter.getParameterAnnotation(ValidPageable.class); + + int pageNumber = Math.max(pageable.getPageNumber(), validPageable != null ? validPageable.page() : 0); + int pageSize = pageable.getPageSize() > 0 + ? pageable.getPageSize() + : validPageable != null ? validPageable.size() : 10; + + return PageRequest.of(pageNumber, pageSize, pageable.getSort()); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/mate/domain/goods/controller/GoodsController.java b/src/main/java/com/example/mate/domain/goods/controller/GoodsController.java index 635d6cff..c8dbb023 100644 --- a/src/main/java/com/example/mate/domain/goods/controller/GoodsController.java +++ b/src/main/java/com/example/mate/domain/goods/controller/GoodsController.java @@ -3,6 +3,7 @@ import com.example.mate.common.response.ApiResponse; import com.example.mate.common.response.PageResponse; import com.example.mate.common.security.auth.AuthMember; +import com.example.mate.common.validator.ValidPageable; import com.example.mate.domain.goods.dto.request.GoodsPostRequest; import com.example.mate.domain.goods.dto.request.GoodsReviewRequest; import com.example.mate.domain.goods.dto.response.GoodsPostResponse; @@ -14,7 +15,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; @@ -84,7 +84,7 @@ public ResponseEntity<ApiResponse<List<GoodsPostSummaryResponse>>> getGoodsPosts public ResponseEntity<ApiResponse<PageResponse<GoodsPostSummaryResponse>>> getGoodsPosts( @Parameter(description = "팀 ID") @RequestParam(required = false) Long teamId, @Parameter(description = "카테고리") @RequestParam(required = false) String category, - @Parameter(description = "페이징 정보", required = true) @PageableDefault Pageable pageable + @Parameter(description = "페이징 정보", required = true) @ValidPageable Pageable pageable ) { PageResponse<GoodsPostSummaryResponse> pageGoodsPosts = goodsService.getPageGoodsPosts(teamId, category, pageable); diff --git a/src/main/java/com/example/mate/domain/goods/dto/request/GoodsPostRequest.java b/src/main/java/com/example/mate/domain/goods/dto/request/GoodsPostRequest.java index db9c2b21..542b6152 100644 --- a/src/main/java/com/example/mate/domain/goods/dto/request/GoodsPostRequest.java +++ b/src/main/java/com/example/mate/domain/goods/dto/request/GoodsPostRequest.java @@ -1,6 +1,6 @@ package com.example.mate.domain.goods.dto.request; -import com.example.mate.common.utils.validator.ValidEnum; +import com.example.mate.common.validator.ValidEnum; import com.example.mate.domain.goods.dto.LocationInfo; import com.example.mate.domain.goods.entity.Category; import com.example.mate.domain.goods.entity.GoodsPost; diff --git a/src/main/java/com/example/mate/domain/goods/dto/request/GoodsReviewRequest.java b/src/main/java/com/example/mate/domain/goods/dto/request/GoodsReviewRequest.java index 10802cc3..12dc621c 100644 --- a/src/main/java/com/example/mate/domain/goods/dto/request/GoodsReviewRequest.java +++ b/src/main/java/com/example/mate/domain/goods/dto/request/GoodsReviewRequest.java @@ -1,6 +1,6 @@ package com.example.mate.domain.goods.dto.request; -import com.example.mate.common.utils.validator.ValidEnum; +import com.example.mate.common.validator.ValidEnum; import com.example.mate.domain.constant.Rating; import com.example.mate.domain.goods.entity.GoodsPost; import com.example.mate.domain.goods.entity.GoodsReview; diff --git a/src/main/java/com/example/mate/domain/goodsChat/controller/GoodsChatRoomController.java b/src/main/java/com/example/mate/domain/goodsChat/controller/GoodsChatRoomController.java index 55c23e1c..727d36e6 100644 --- a/src/main/java/com/example/mate/domain/goodsChat/controller/GoodsChatRoomController.java +++ b/src/main/java/com/example/mate/domain/goodsChat/controller/GoodsChatRoomController.java @@ -3,15 +3,18 @@ import com.example.mate.common.response.ApiResponse; import com.example.mate.common.response.PageResponse; import com.example.mate.common.security.auth.AuthMember; +import com.example.mate.common.validator.ValidPageable; import com.example.mate.domain.goodsChat.dto.response.GoodsChatMessageResponse; import com.example.mate.domain.goodsChat.dto.response.GoodsChatRoomResponse; import com.example.mate.domain.goodsChat.dto.response.GoodsChatRoomSummaryResponse; import com.example.mate.domain.goodsChat.service.GoodsChatService; import com.example.mate.domain.member.dto.response.MemberSummaryResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; @@ -25,51 +28,68 @@ @RestController @RequiredArgsConstructor @RequestMapping("/api/goods/chat") +@Tag(name = "GoodsChatRoomController", description = "굿즈거래 채팅방 관련 API") public class GoodsChatRoomController { private final GoodsChatService goodsChatService; @PostMapping - public ResponseEntity<ApiResponse<GoodsChatRoomResponse>> createGoodsChatRoom(@AuthenticationPrincipal AuthMember member, - @RequestParam Long goodsPostId) { + @Operation(summary = "굿즈거래 채팅방 입장 및 생성", description = "굿즈 거래 게시글에 대한 채팅방을 생성하거나 기존 채팅방 정보를 조회합니다.") + public ResponseEntity<ApiResponse<GoodsChatRoomResponse>> createGoodsChatRoom( + @AuthenticationPrincipal AuthMember member, + @Parameter(description = "판매글 ID", required = true) @RequestParam Long goodsPostId + ) { GoodsChatRoomResponse response = goodsChatService.getOrCreateGoodsChatRoom(member.getMemberId(), goodsPostId); return ResponseEntity.ok(ApiResponse.success(response)); } @GetMapping("/{chatRoomId}/message") + @Operation(summary = "굿즈거래 채팅방 메시지 조회", description = "지정된 채팅방의 메시지를 페이징 처리하여 조회합니다.") public ResponseEntity<ApiResponse<PageResponse<GoodsChatMessageResponse>>> getGoodsChatRoomMessages( @AuthenticationPrincipal AuthMember member, - @PathVariable Long chatRoomId, - @PageableDefault(page = 1, size = 20) Pageable pageable + @Parameter(description = "채팅방 ID", required = true) @PathVariable Long chatRoomId, + @Parameter(description = "페이징 정보") @ValidPageable Pageable pageable ) { PageResponse<GoodsChatMessageResponse> response = goodsChatService.getMessagesForChatRoom(chatRoomId, member.getMemberId(), pageable); return ResponseEntity.ok(ApiResponse.success(response)); } @GetMapping - public ResponseEntity<ApiResponse<PageResponse<GoodsChatRoomSummaryResponse>>> getGoodsChatRooms(@AuthenticationPrincipal AuthMember member, - @PageableDefault Pageable pageable) { + @Operation(summary = "사용자의 굿즈거래 채팅방 목록 조회", description = "사용자가 참여 중인 굿즈거래 채팅방 목록을 조회합니다.") + public ResponseEntity<ApiResponse<PageResponse<GoodsChatRoomSummaryResponse>>> getGoodsChatRooms( + @AuthenticationPrincipal AuthMember member, + @Parameter(description = "페이징 정보") @ValidPageable Pageable pageable + ) { PageResponse<GoodsChatRoomSummaryResponse> response = goodsChatService.getGoodsChatRooms(member.getMemberId(), pageable); return ResponseEntity.ok(ApiResponse.success(response)); } @DeleteMapping("/{chatRoomId}") - public ResponseEntity<Void> leaveGoodsChatRoom(@AuthenticationPrincipal AuthMember member, @PathVariable Long chatRoomId) { + @Operation(summary = "굿즈거래 채팅방 나가기", description = "사용자가 지정된 굿즈거래 채팅방을 나갑니다. 만약 모든 사용자가 나가면 채팅방이 삭제됩니다.") + public ResponseEntity<Void> leaveGoodsChatRoom( + @AuthenticationPrincipal AuthMember member, + @Parameter(description = "채팅방 ID", required = true) @PathVariable Long chatRoomId + ) { goodsChatService.deactivateGoodsChatPart(member.getMemberId(), chatRoomId); - return ResponseEntity.noContent().build(); } @GetMapping("/{chatRoomId}") - public ResponseEntity<ApiResponse<GoodsChatRoomResponse>> getGoodsChatRoomInfo(@AuthenticationPrincipal AuthMember member, - @PathVariable Long chatRoomId) { + @Operation(summary = "굿즈거래 채팅방 입장", description = "굿즈 거래 채팅방의 정보를 조회합니다.") + public ResponseEntity<ApiResponse<GoodsChatRoomResponse>> getGoodsChatRoomInfo( + @AuthenticationPrincipal AuthMember member, + @Parameter(description = "채팅방 ID", required = true) @PathVariable Long chatRoomId + ) { GoodsChatRoomResponse response = goodsChatService.getGoodsChatRoomInfo(member.getMemberId(), chatRoomId); return ResponseEntity.ok(ApiResponse.success(response)); } @GetMapping("/{chatRoomId}/members") - public ResponseEntity<ApiResponse<List<MemberSummaryResponse>>> getGoodsChatRoomMembers(@AuthenticationPrincipal AuthMember member, - @PathVariable Long chatRoomId) { + @Operation(summary = "굿즈거래 채팅방 인원 조회", description = "지정된 채팅방에 참여 중인 사용자 목록을 조회합니다.") + public ResponseEntity<ApiResponse<List<MemberSummaryResponse>>> getGoodsChatRoomMembers( + @AuthenticationPrincipal AuthMember member, + @Parameter(description = "채팅방 ID", required = true) @PathVariable Long chatRoomId + ) { List<MemberSummaryResponse> responses = goodsChatService.getChatRoomMembers(member.getMemberId(), chatRoomId); return ResponseEntity.ok(ApiResponse.success(responses)); } diff --git a/src/main/java/com/example/mate/domain/mate/controller/MateController.java b/src/main/java/com/example/mate/domain/mate/controller/MateController.java index df5bba30..1f656353 100644 --- a/src/main/java/com/example/mate/domain/mate/controller/MateController.java +++ b/src/main/java/com/example/mate/domain/mate/controller/MateController.java @@ -2,7 +2,7 @@ import com.example.mate.common.response.ApiResponse; import com.example.mate.common.response.PageResponse; -import com.example.mate.common.security.auth.AuthMember; +import com.example.mate.common.validator.ValidPageable; import com.example.mate.domain.constant.Gender; import com.example.mate.domain.mate.dto.request.*; import com.example.mate.domain.mate.dto.response.*; @@ -16,10 +16,8 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -35,12 +33,12 @@ public class MateController { @PostMapping @Operation(summary = "메이트 구인글 등록", description = "메이트 구인글 페이지에서 등록합니다.") - public ResponseEntity<ApiResponse<MatePostResponse>> createMatePost(@Parameter(description = "구인글 등록 데이터", required = true) - @Valid @RequestPart(value = "data") MatePostCreateRequest request, - @Parameter(description = "구인글 대표사진", required = true) - @RequestPart(value = "file", required = false) MultipartFile file, - @AuthenticationPrincipal AuthMember member) { - MatePostResponse response = mateService.createMatePost(request, file, member.getMemberId()); + public ResponseEntity<ApiResponse<MatePostResponse>> createMatePost( + @Parameter(description = "구인글 등록 데이터", required = true) @Valid @RequestPart(value = "data") MatePostCreateRequest request, + @Parameter(description = "구인글 대표사진", required = true) @RequestPart(value = "file", required = false) MultipartFile file + ) { + //TODO - member 정보를 request가 아니라 @AuthenticationPrincipal Long memberId로 받도록 변경 + MatePostResponse response = mateService.createMatePost(request, file); return ResponseEntity.ok(ApiResponse.success(response)); } @@ -54,21 +52,15 @@ public ResponseEntity<ApiResponse<List<MatePostSummaryResponse>>> getMainPagePos @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) @Operation(summary = "메이트 구인글 페이징 조회", description = "메이트 구인글 페이지에서 팀/카테고리 기준으로 페이징 조회합니다.") - public ResponseEntity<ApiResponse<PageResponse<MatePostSummaryResponse>>> getMatePagePosts(@Parameter(description = "팀 ID") - @RequestParam(required = false) Long teamId, - @Parameter(description = "정렬 기준") - @RequestParam(required = false) String sortType, - @Parameter(description = "연령대 카테고리") - @RequestParam(required = false) String age, - @Parameter(description = "성별 카테고리") - @RequestParam(required = false) String gender, - @Parameter(description = "모집인원 수") - @RequestParam(required = false) Integer maxParticipants, - @Parameter(description = "이동수단 카테고리") - @RequestParam(required = false) String transportType, - @Parameter(description = "페이징 정보") - @PageableDefault Pageable pageable) { - + public ResponseEntity<ApiResponse<PageResponse<MatePostSummaryResponse>>> getMatePagePosts( + @Parameter(description = "팀 ID") @RequestParam(required = false) Long teamId, + @Parameter(description = "정렬 기준") @RequestParam(required = false) String sortType, + @Parameter(description = "연령대 카테고리") @RequestParam(required = false) String age, + @Parameter(description = "성별 카테고리") @RequestParam(required = false) String gender, + @Parameter(description = "모집인원 수") @RequestParam(required = false) Integer maxParticipants, + @Parameter(description = "이동수단 카테고리") @RequestParam(required = false) String transportType, + @Parameter(description = "페이징 정보") @ValidPageable Pageable pageable + ) { MatePostSearchRequest request = MatePostSearchRequest.builder() .teamId(teamId) .sortType(sortType != null ? SortType.from(sortType) : null) @@ -91,66 +83,65 @@ public ResponseEntity<ApiResponse<MatePostDetailResponse>> getMatePostDetail(@Pa return ResponseEntity.ok(ApiResponse.success(response)); } - @PutMapping("/{postId}") + @PatchMapping("/{memberId}/{postId}") @Operation(summary = "메이트 구인글 수정", description = "메이트 구인글 상세 페이지에서 수정합니다.") - public ResponseEntity<ApiResponse<MatePostResponse>> updateMatePost( @AuthenticationPrincipal AuthMember member, - @Parameter(description = "구인글 ID", required = true) - @PathVariable Long postId, - @Parameter(description = "수정할 구인글 데이터", required = true) - @Valid @RequestPart(value = "data") MatePostUpdateRequest request, - @Parameter(description = "수정할 대표사진 파일 ", required = true) - @RequestPart(value = "file", required = false) MultipartFile file) { - - MatePostResponse response = mateService.updateMatePost(member.getMemberId(), postId, request, file); + public ResponseEntity<ApiResponse<MatePostResponse>> updateMatePost( + @Parameter(description = "작성자 ID (삭제 예정)", required = true) @PathVariable Long memberId, + @Parameter(description = "구인글 ID", required = true) @PathVariable Long postId, + @Parameter(description = "수정할 구인글 데이터", required = true) @Valid @RequestPart(value = "data") MatePostUpdateRequest request, + @Parameter(description = "수정할 대표사진 파일 ", required = true) @RequestPart(value = "file", required = false) MultipartFile file + ) { + MatePostResponse response = mateService.updateMatePost(memberId, postId, request, file); return ResponseEntity.ok(ApiResponse.success(response)); } + // TODO: @PathVariable Long memberId -> @AuthenticationPrincipal 로 변경 // 메이트 게시글 모집 상태 변경 - @PatchMapping("/{postId}/status") + @PatchMapping("/{memberId}/{postId}/status") @Operation(summary = "메이트 구인글 모집상태 변경", description = "메이트 구인글 채팅방에서 모집상태를 변경합니다.") - public ResponseEntity<ApiResponse<MatePostResponse>> updateMatePostStatus( @AuthenticationPrincipal AuthMember member, - @Parameter(description = "구인글 ID", required = true) - @PathVariable(value = "postId") Long postId, - @Parameter(description = "변경할 모집상태와 현재 참여자 리스트 ID", required = true) - @Valid @RequestBody MatePostStatusRequest request) { - - MatePostResponse response = mateService.updateMatePostStatus(member.getMemberId(), postId, request); + public ResponseEntity<ApiResponse<MatePostResponse>> updateMatePostStatus( + @Parameter(description = "작성자 ID (삭제 예정)", required = true) @PathVariable(value = "memberId") Long memberId, + @Parameter(description = "구인글 ID", required = true) @PathVariable(value = "postId") Long postId, + @Parameter(description = "변경할 모집상태와 현재 참여자 리스트 ID", required = true) @Valid @RequestBody MatePostStatusRequest request + ) { + MatePostResponse response = mateService.updateMatePostStatus(memberId, postId, request); return ResponseEntity.ok(ApiResponse.success(response)); } - @DeleteMapping("/{postId}") + // TODO: @PathVariable Long memberId -> @AuthenticationPrincipal 로 변경 + @DeleteMapping("/{memberId}/{postId}") @Operation(summary = "메이트 구인글 삭제", description = "메이트 구인글 상세 페이지에서 삭제합니다.") - public ResponseEntity<Void> deleteMatePost( @AuthenticationPrincipal AuthMember member, + public ResponseEntity<Void> deleteMatePost(@Parameter(description = "작성자 ID (삭제 예정)", required = true) + @PathVariable Long memberId, @Parameter(description = "삭제할 구인글 ID", required = true) - @PathVariable Long postId) { + @PathVariable Long postId) { - mateService.deleteMatePost(member.getMemberId(), postId); + mateService.deleteMatePost(memberId, postId); return ResponseEntity.noContent().build(); } - @PatchMapping("/{postId}/complete") + // TODO: @PathVariable Long memberId -> @AuthenticationPrincipal 로 변경 + @PatchMapping("/{memberId}/{postId}/complete") @Operation(summary = "직관완료 처리", description = "메이트 구인글 채팅방에서 직관완료 처리를 진행합니다.") - public ResponseEntity<ApiResponse<MatePostCompleteResponse>> completeVisit( @AuthenticationPrincipal AuthMember member, - @Parameter(description = "구인글 ID", required = true) - @PathVariable Long postId, - @Parameter(description = "실제 직관 참여자 리스트 ID", required = true) - @Valid @RequestBody MatePostCompleteRequest request) { - - MatePostCompleteResponse response = mateService.completeVisit(member.getMemberId(), postId, request); + public ResponseEntity<ApiResponse<MatePostCompleteResponse>> completeVisit( + @Parameter(description = "작성자 ID (삭제 예정)", required = true) @PathVariable Long memberId, + @Parameter(description = "구인글 ID", required = true) @PathVariable Long postId, + @Parameter(description = "실제 직관 참여자 리스트 ID", required = true) @Valid @RequestBody MatePostCompleteRequest request + ) { + MatePostCompleteResponse response = mateService.completeVisit(memberId, postId, request); return ResponseEntity.ok(ApiResponse.success(response)); } - @PostMapping("/{postId}/reviews") + // TODO: @PathVariable Long memberId -> @AuthenticationPrincipal 로 변경 + @PostMapping("/{memberId}/{postId}/reviews") @Operation(summary = "메이트 직관 후기 등록", description = "직관 타임라인 페이지에서 메이트에 대한 후기를 등록합니다.") - public ResponseEntity<ApiResponse<MateReviewCreateResponse>> createMateReview( @AuthenticationPrincipal AuthMember member, - @Parameter(description = "구인글 ID", required = true) - @PathVariable Long postId, - @Parameter(description = "리뷰 대상자 ID와 평점 및 코멘트", required = true) - @Valid @RequestBody MateReviewCreateRequest request + public ResponseEntity<ApiResponse<MateReviewCreateResponse>> createMateReview( + @Parameter(description = "작성자 ID (삭제 예정)", required = true) @PathVariable Long memberId, + @Parameter(description = "구인글 ID", required = true) @PathVariable Long postId, + @Parameter(description = "리뷰 대상자 ID와 평점 및 코멘트", required = true) @Valid @RequestBody MateReviewCreateRequest request ) { - - MateReviewCreateResponse response = mateService.createReview(postId, member.getMemberId(), request); + MateReviewCreateResponse response = mateService.createReview(postId, memberId, request); return ResponseEntity.ok(ApiResponse.success(response)); } } \ No newline at end of file diff --git a/src/main/java/com/example/mate/domain/mate/dto/request/MatePostCreateRequest.java b/src/main/java/com/example/mate/domain/mate/dto/request/MatePostCreateRequest.java index 77f593e9..3748d003 100644 --- a/src/main/java/com/example/mate/domain/mate/dto/request/MatePostCreateRequest.java +++ b/src/main/java/com/example/mate/domain/mate/dto/request/MatePostCreateRequest.java @@ -19,6 +19,7 @@ @NoArgsConstructor @AllArgsConstructor public class MatePostCreateRequest { + private Long memberId; @NotNull(message = "팀 ID는 필수 입력 값입니다.") @Min(value = 0, message = "팀 ID는 0 이상이어야 합니다.") diff --git a/src/main/java/com/example/mate/domain/mate/dto/request/MatePostSearchRequest.java b/src/main/java/com/example/mate/domain/mate/dto/request/MatePostSearchRequest.java index 75b01fa5..d6e67c33 100644 --- a/src/main/java/com/example/mate/domain/mate/dto/request/MatePostSearchRequest.java +++ b/src/main/java/com/example/mate/domain/mate/dto/request/MatePostSearchRequest.java @@ -1,6 +1,6 @@ package com.example.mate.domain.mate.dto.request; -import com.example.mate.common.utils.validator.ValidEnum; +import com.example.mate.common.validator.ValidEnum; import com.example.mate.domain.constant.Gender; import com.example.mate.domain.mate.entity.Age; import com.example.mate.domain.mate.entity.SortType; diff --git a/src/main/java/com/example/mate/domain/mate/dto/request/MatePostStatusRequest.java b/src/main/java/com/example/mate/domain/mate/dto/request/MatePostStatusRequest.java index 91adc734..3bc80432 100644 --- a/src/main/java/com/example/mate/domain/mate/dto/request/MatePostStatusRequest.java +++ b/src/main/java/com/example/mate/domain/mate/dto/request/MatePostStatusRequest.java @@ -1,6 +1,6 @@ package com.example.mate.domain.mate.dto.request; -import com.example.mate.common.utils.validator.ValidEnum; +import com.example.mate.common.validator.ValidEnum; import com.example.mate.domain.mate.entity.Status; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; diff --git a/src/main/java/com/example/mate/domain/mate/dto/request/MatePostUpdateRequest.java b/src/main/java/com/example/mate/domain/mate/dto/request/MatePostUpdateRequest.java index e553f3e5..33c7307f 100644 --- a/src/main/java/com/example/mate/domain/mate/dto/request/MatePostUpdateRequest.java +++ b/src/main/java/com/example/mate/domain/mate/dto/request/MatePostUpdateRequest.java @@ -1,6 +1,6 @@ package com.example.mate.domain.mate.dto.request; -import com.example.mate.common.utils.validator.ValidEnum; +import com.example.mate.common.validator.ValidEnum; import com.example.mate.domain.constant.Gender; import com.example.mate.domain.mate.entity.Age; import com.example.mate.domain.mate.entity.TransportType; diff --git a/src/main/java/com/example/mate/domain/mate/service/MateService.java b/src/main/java/com/example/mate/domain/mate/service/MateService.java index 95b6caa3..245d7619 100644 --- a/src/main/java/com/example/mate/domain/mate/service/MateService.java +++ b/src/main/java/com/example/mate/domain/mate/service/MateService.java @@ -41,8 +41,8 @@ public class MateService { private final MateReviewRepository mateReviewRepository; private final FileService fileService; - public MatePostResponse createMatePost(MatePostCreateRequest request, MultipartFile file, Long memberId) { - Member author = findMemberById(memberId); + public MatePostResponse createMatePost(MatePostCreateRequest request, MultipartFile file) { + Member author = findMemberById(request.getMemberId()); Match match = findMatchById(request.getMatchId()); diff --git a/src/main/java/com/example/mate/domain/mateChat/controller/MateChatRoomController.java b/src/main/java/com/example/mate/domain/mateChat/controller/MateChatRoomController.java index 105da16e..69697ee2 100644 --- a/src/main/java/com/example/mate/domain/mateChat/controller/MateChatRoomController.java +++ b/src/main/java/com/example/mate/domain/mateChat/controller/MateChatRoomController.java @@ -3,6 +3,7 @@ import com.example.mate.common.response.ApiResponse; import com.example.mate.common.response.PageResponse; import com.example.mate.common.security.auth.AuthMember; +import com.example.mate.common.validator.ValidPageable; import com.example.mate.domain.mateChat.dto.response.MateChatMessageResponse; import com.example.mate.domain.mateChat.dto.response.MateChatRoomListResponse; import com.example.mate.domain.mateChat.dto.response.MateChatRoomResponse; @@ -13,7 +14,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -31,7 +31,7 @@ public class MateChatRoomController { @Operation(summary = "메이트 게시글 -> 채팅방 생성/입장", description = "메이트 게시글 페이지에서 채팅방으로 입장") public ResponseEntity<ApiResponse<MateChatRoomResponse>> createOrJoinChatRoomFromPost( @Parameter(description = "메이트 게시글 ID") @PathVariable Long matePostId, - @AuthenticationPrincipal AuthMember member + @AuthenticationPrincipal AuthMember member ) { MateChatRoomResponse response = chatRoomService.createOrJoinChatRoomFromPost(matePostId, member.getMemberId()); return ResponseEntity.ok(ApiResponse.success(response)); @@ -40,10 +40,9 @@ public ResponseEntity<ApiResponse<MateChatRoomResponse>> createOrJoinChatRoomFro @GetMapping("/me") @Operation(summary = "채팅방 목록 조회", description = "사용자의 채팅방 목록을 조회합니다.") public ResponseEntity<ApiResponse<PageResponse<MateChatRoomListResponse>>> getMyChatRooms( - @Parameter(description = "페이지 정보") @PageableDefault Pageable pageable, - @AuthenticationPrincipal AuthMember member - - ) { + @Parameter(description = "페이지 정보") @ValidPageable Pageable pageable, + @AuthenticationPrincipal AuthMember member + ) { PageResponse<MateChatRoomListResponse> response = chatRoomService.getMyChatRooms(member.getMemberId(), pageable); return ResponseEntity.ok(ApiResponse.success(response)); } @@ -62,8 +61,8 @@ public ResponseEntity<ApiResponse<MateChatRoomResponse>> joinExistingChatRoom( @Operation(summary = "채팅방 메세지 조회", description = "메시지 내역을 조회합니다.") public ResponseEntity<ApiResponse<PageResponse<MateChatMessageResponse>>> getChatMessages( @Parameter(description = "채팅방 ID") @PathVariable Long chatroomId, - @AuthenticationPrincipal AuthMember member, - @Parameter(description = "페이지 정보") @PageableDefault(page = 1, size = 20) Pageable pageable + @AuthenticationPrincipal AuthMember member, + @Parameter(description = "페이지 정보") @ValidPageable(page = 1, size = 20) Pageable pageable ) { PageResponse<MateChatMessageResponse> messages = chatRoomService.getChatMessages(chatroomId, member.getMemberId(), pageable); return ResponseEntity.ok(ApiResponse.success(messages)); @@ -73,7 +72,7 @@ public ResponseEntity<ApiResponse<PageResponse<MateChatMessageResponse>>> getCha @Operation(summary = "채팅방 나가기", description = "채팅방에서 퇴장합니다.") public ResponseEntity<Void> leaveChatRoom( @Parameter(description = "채팅방 ID") @PathVariable Long chatroomId, - @AuthenticationPrincipal AuthMember member + @AuthenticationPrincipal AuthMember member ) { chatRoomService.leaveChatRoom(chatroomId, member.getMemberId()); return ResponseEntity.noContent().build(); diff --git a/src/main/java/com/example/mate/domain/member/controller/FollowController.java b/src/main/java/com/example/mate/domain/member/controller/FollowController.java index 191e7a29..08048be8 100644 --- a/src/main/java/com/example/mate/domain/member/controller/FollowController.java +++ b/src/main/java/com/example/mate/domain/member/controller/FollowController.java @@ -1,10 +1,9 @@ package com.example.mate.domain.member.controller; -import static com.example.mate.common.response.PageResponse.validatePageable; - import com.example.mate.common.response.ApiResponse; import com.example.mate.common.response.PageResponse; import com.example.mate.common.security.auth.AuthMember; +import com.example.mate.common.validator.ValidPageable; import com.example.mate.domain.member.dto.response.MemberSummaryResponse; import com.example.mate.domain.member.service.FollowService; import io.swagger.v3.oas.annotations.Operation; @@ -12,7 +11,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.DeleteMapping; @@ -52,9 +50,8 @@ public ResponseEntity<Void> unfollowMember( @GetMapping("{memberId}/followings") public ResponseEntity<ApiResponse<PageResponse<MemberSummaryResponse>>> getFollowings( @Parameter(description = "특정 회원 ID") @PathVariable Long memberId, - @Parameter(description = "페이지 요청 정보") @PageableDefault Pageable pageable + @Parameter(description = "페이지 요청 정보") @ValidPageable Pageable pageable ) { - pageable = validatePageable(pageable); PageResponse<MemberSummaryResponse> response = followService.getFollowingsPage(memberId, pageable); return ResponseEntity.ok(ApiResponse.success(response)); } @@ -63,9 +60,8 @@ public ResponseEntity<ApiResponse<PageResponse<MemberSummaryResponse>>> getFollo @GetMapping("{memberId}/followers") public ResponseEntity<ApiResponse<PageResponse<MemberSummaryResponse>>> getFollowers( @Parameter(description = "특정 회원 ID") @PathVariable Long memberId, - @Parameter(description = "페이지 요청 정보") @PageableDefault Pageable pageable + @Parameter(description = "페이지 요청 정보") @ValidPageable Pageable pageable ) { - pageable = validatePageable(pageable); PageResponse<MemberSummaryResponse> response = followService.getFollowersPage(memberId, pageable); return ResponseEntity.ok(ApiResponse.success(response)); diff --git a/src/main/java/com/example/mate/domain/member/controller/MemberController.java b/src/main/java/com/example/mate/domain/member/controller/MemberController.java index f81207f0..cdec1540 100644 --- a/src/main/java/com/example/mate/domain/member/controller/MemberController.java +++ b/src/main/java/com/example/mate/domain/member/controller/MemberController.java @@ -9,6 +9,7 @@ import com.example.mate.domain.member.dto.response.MemberLoginResponse; import com.example.mate.domain.member.dto.response.MemberProfileResponse; import com.example.mate.domain.member.dto.response.MyProfileResponse; +import com.example.mate.domain.member.service.LogoutRedisService; import com.example.mate.domain.member.service.MemberService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -27,6 +28,7 @@ public class MemberController { private final MemberService memberService; + private final LogoutRedisService logoutRedisService; @Operation(summary = "자체 회원가입 기능") @PostMapping("/join") @@ -45,6 +47,15 @@ public ResponseEntity<ApiResponse<MemberLoginResponse>> catchMiLogin( return ResponseEntity.ok(ApiResponse.success(response)); } + @Operation(summary = "CATCH Mi 서비스 로그아웃", description = "캐치미 서비스에 로그아웃합니다.") + @PostMapping("/logout") + public ResponseEntity<Void> catchMiLogout( + @Parameter(description = "회원 로그인 토큰 헤더", required = true) @RequestHeader("Authorization") String token + ) { + logoutRedisService.addTokenToBlacklist(token); + return ResponseEntity.noContent().build(); + } + @Operation(summary = "내 프로필 조회") @GetMapping("/me") public ResponseEntity<ApiResponse<MyProfileResponse>> findMyInfo( diff --git a/src/main/java/com/example/mate/domain/member/controller/ProfileController.java b/src/main/java/com/example/mate/domain/member/controller/ProfileController.java index 6de3c51c..c2140ba6 100644 --- a/src/main/java/com/example/mate/domain/member/controller/ProfileController.java +++ b/src/main/java/com/example/mate/domain/member/controller/ProfileController.java @@ -1,12 +1,11 @@ package com.example.mate.domain.member.controller; -import static com.example.mate.common.response.PageResponse.validatePageable; - import com.example.mate.common.error.CustomException; import com.example.mate.common.error.ErrorCode; import com.example.mate.common.response.ApiResponse; import com.example.mate.common.response.PageResponse; import com.example.mate.common.security.auth.AuthMember; +import com.example.mate.common.validator.ValidPageable; import com.example.mate.domain.member.dto.response.MyGoodsRecordResponse; import com.example.mate.domain.member.dto.response.MyReviewResponse; import com.example.mate.domain.member.dto.response.MyVisitResponse; @@ -16,7 +15,6 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; @@ -36,9 +34,8 @@ public class ProfileController { @GetMapping("/{memberId}/review/goods") public ResponseEntity<ApiResponse<PageResponse<MyReviewResponse>>> getGoodsReviews( @Parameter(description = "회원 ID") @PathVariable Long memberId, - @Parameter(description = "페이지 요청 정보") @PageableDefault Pageable pageable + @Parameter(description = "페이지 요청 정보") @ValidPageable Pageable pageable ) { - validatePageable(pageable); PageResponse<MyReviewResponse> response = profileService.getGoodsReviewPage(memberId, pageable); return ResponseEntity.ok(ApiResponse.success(response)); } @@ -47,9 +44,8 @@ public ResponseEntity<ApiResponse<PageResponse<MyReviewResponse>>> getGoodsRevie @GetMapping("{memberId}/review/mate") public ResponseEntity<ApiResponse<PageResponse<MyReviewResponse>>> getMateReviews( @Parameter(description = "회원 ID") @PathVariable Long memberId, - @Parameter(description = "페이지 요청 정보") @PageableDefault Pageable pageable + @Parameter(description = "페이지 요청 정보") @ValidPageable Pageable pageable ) { - validatePageable(pageable); PageResponse<MyReviewResponse> response = profileService.getMateReviewPage(memberId, pageable); return ResponseEntity.ok(ApiResponse.success(response)); } @@ -58,9 +54,8 @@ public ResponseEntity<ApiResponse<PageResponse<MyReviewResponse>>> getMateReview @GetMapping("/timeline") public ResponseEntity<ApiResponse<PageResponse<MyVisitResponse>>> getMyVisits( @Parameter(description = "회원 로그인 정보") @AuthenticationPrincipal AuthMember authMember, - @Parameter(description = "페이지 요청 정보") @PageableDefault Pageable pageable + @Parameter(description = "페이지 요청 정보") @ValidPageable Pageable pageable ) { - validatePageable(pageable); PageResponse<MyVisitResponse> response = profileService.getMyVisitPage(authMember.getMemberId(), pageable); return ResponseEntity.ok(ApiResponse.success(response)); } @@ -69,9 +64,8 @@ public ResponseEntity<ApiResponse<PageResponse<MyVisitResponse>>> getMyVisits( @GetMapping("/{memberId}/goods/sold") public ResponseEntity<ApiResponse<PageResponse<MyGoodsRecordResponse>>> getSoldGoods( @Parameter(description = "회원 ID") @PathVariable Long memberId, - @Parameter(description = "페이지 요청 정보") @PageableDefault Pageable pageable + @Parameter(description = "페이지 요청 정보") @ValidPageable Pageable pageable ) { - pageable = validatePageable(pageable); PageResponse<MyGoodsRecordResponse> response = profileService.getSoldGoodsPage(memberId, pageable); return ResponseEntity.ok(ApiResponse.success(response)); } @@ -80,13 +74,12 @@ public ResponseEntity<ApiResponse<PageResponse<MyGoodsRecordResponse>>> getSoldG @GetMapping("/{memberId}/goods/bought") public ResponseEntity<ApiResponse<PageResponse<MyGoodsRecordResponse>>> getBoughtGoods( @Parameter(description = "회원 ID") @PathVariable Long memberId, - @Parameter(description = "페이지 요청 정보") @PageableDefault Pageable pageable, + @Parameter(description = "페이지 요청 정보") @ValidPageable Pageable pageable, @Parameter(description = "회원 로그인 정보") @AuthenticationPrincipal AuthMember authMember ) { if (!authMember.getMemberId().equals(memberId)) { throw new CustomException(ErrorCode.MEMBER_UNAUTHORIZED_ACCESS); } - pageable = validatePageable(pageable); PageResponse<MyGoodsRecordResponse> response = profileService.getBoughtGoodsPage(memberId, pageable); return ResponseEntity.ok(ApiResponse.success(response)); } diff --git a/src/main/java/com/example/mate/domain/member/service/FollowService.java b/src/main/java/com/example/mate/domain/member/service/FollowService.java index 2536ec16..af18f517 100644 --- a/src/main/java/com/example/mate/domain/member/service/FollowService.java +++ b/src/main/java/com/example/mate/domain/member/service/FollowService.java @@ -56,14 +56,7 @@ public PageResponse<MemberSummaryResponse> getFollowingsPage(Long memberId, Page .map(MemberSummaryResponse::from) .toList(); - return PageResponse.<MemberSummaryResponse>builder() - .content(content) - .totalPages(followingsPage.getTotalPages()) - .totalElements(followingsPage.getTotalElements()) - .hasNext(followingsPage.hasNext()) - .pageNumber(followingsPage.getNumber()) - .pageSize(followingsPage.getSize()) - .build(); + return PageResponse.from(followingsPage, content); } // 특정 회원의 팔로워 리스트 페이징 조회 @@ -78,14 +71,7 @@ public PageResponse<MemberSummaryResponse> getFollowersPage(Long memberId, Pagea .map(MemberSummaryResponse::from) .toList(); - return PageResponse.<MemberSummaryResponse>builder() - .content(content) - .totalPages(followingsPage.getTotalPages()) - .totalElements(followingsPage.getTotalElements()) - .hasNext(followingsPage.hasNext()) - .pageNumber(followingsPage.getNumber()) - .pageSize(followingsPage.getSize()) - .build(); + return PageResponse.from(followingsPage, content); } private Map<String, Member> isValidMemberFollow(Long followerId, Long followingId) { diff --git a/src/main/java/com/example/mate/domain/member/service/LogoutRedisService.java b/src/main/java/com/example/mate/domain/member/service/LogoutRedisService.java new file mode 100644 index 00000000..eb4a7f74 --- /dev/null +++ b/src/main/java/com/example/mate/domain/member/service/LogoutRedisService.java @@ -0,0 +1,31 @@ +package com.example.mate.domain.member.service; + +import com.example.mate.common.error.CustomException; +import com.example.mate.common.error.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +public class LogoutRedisService { + + private final RedisTemplate<String, String> redisTemplate; + + // 로그아웃 시 블랙리스트에 토큰 추가 + public void addTokenToBlacklist(String token) { + if (token == null || !token.startsWith("Bearer ")) { + throw new CustomException(ErrorCode.INVALID_AUTH_TOKEN); + } + + // TODO : 테스트용 1분 유효를 변경 + redisTemplate.opsForValue().set("blacklist:" + token.substring(7), "blacklisted", 1, TimeUnit.MINUTES); + } + + // 블랙리스트에 토큰 있는지 확인 + public boolean isTokenBlacklisted(String accessToken) { + return redisTemplate.hasKey("blacklist:" + accessToken); + } +} diff --git a/src/main/java/com/example/mate/domain/member/service/ProfileService.java b/src/main/java/com/example/mate/domain/member/service/ProfileService.java index efed4fc9..0df2fe3e 100644 --- a/src/main/java/com/example/mate/domain/member/service/ProfileService.java +++ b/src/main/java/com/example/mate/domain/member/service/ProfileService.java @@ -4,7 +4,6 @@ import com.example.mate.common.error.ErrorCode; import com.example.mate.common.response.PageResponse; import com.example.mate.domain.goods.entity.GoodsPost; -import com.example.mate.domain.goods.entity.GoodsPostImage; import com.example.mate.domain.goods.entity.Status; import com.example.mate.domain.goods.repository.GoodsPostRepository; import com.example.mate.domain.goods.repository.GoodsReviewRepositoryCustom; @@ -56,14 +55,7 @@ public PageResponse<MyGoodsRecordResponse> getSoldGoodsPage(Long memberId, Pagea List<MyGoodsRecordResponse> content = soldGoodsPage.getContent().stream() .map(this::convertToRecordResponse).toList(); - return PageResponse.<MyGoodsRecordResponse>builder() - .content(content) - .totalPages(soldGoodsPage.getTotalPages()) - .totalElements(soldGoodsPage.getTotalElements()) - .hasNext(soldGoodsPage.hasNext()) - .pageNumber(soldGoodsPage.getNumber()) - .pageSize(soldGoodsPage.getSize()) - .build(); + return PageResponse.from(soldGoodsPage, content); } // 굿즈 구매기록 페이징 조회 @@ -77,14 +69,7 @@ public PageResponse<MyGoodsRecordResponse> getBoughtGoodsPage(Long memberId, Pag List<MyGoodsRecordResponse> content = boughtGoodsPage.getContent().stream() .map(this::convertToRecordResponse).toList(); - return PageResponse.<MyGoodsRecordResponse>builder() - .content(content) - .totalPages(boughtGoodsPage.getTotalPages()) - .totalElements(boughtGoodsPage.getTotalElements()) - .hasNext(boughtGoodsPage.hasNext()) - .pageNumber(boughtGoodsPage.getNumber()) - .pageSize(boughtGoodsPage.getSize()) - .build(); + return PageResponse.from(boughtGoodsPage, content); } private void validateMemberId(Long memberId) { @@ -105,14 +90,7 @@ public PageResponse<MyReviewResponse> getMateReviewPage(Long memberId, Pageable Page<MyReviewResponse> mateReviewPage = mateReviewRepositoryCustom.findMateReviewsByRevieweeId( memberId, pageable); - return PageResponse.<MyReviewResponse>builder() - .content(mateReviewPage.getContent()) - .totalPages(mateReviewPage.getTotalPages()) - .totalElements(mateReviewPage.getTotalElements()) - .hasNext(mateReviewPage.hasNext()) - .pageNumber(mateReviewPage.getNumber()) - .pageSize(mateReviewPage.getSize()) - .build(); + return PageResponse.from(mateReviewPage); } // 굿즈거래 후기 페이징 조회 @@ -123,14 +101,7 @@ public PageResponse<MyReviewResponse> getGoodsReviewPage(Long memberId, Pageable Page<MyReviewResponse> goodsReviewPage = goodsReviewRepositoryCustom.findGoodsReviewsByRevieweeId( memberId, pageable); - return PageResponse.<MyReviewResponse>builder() - .content(goodsReviewPage.getContent()) - .totalPages(goodsReviewPage.getTotalPages()) - .totalElements(goodsReviewPage.getTotalElements()) - .hasNext(goodsReviewPage.hasNext()) - .pageNumber(goodsReviewPage.getNumber()) - .pageSize(goodsReviewPage.getSize()) - .build(); + return PageResponse.from(goodsReviewPage); } // TODO : 쿼리가 너무 많이 나오는 문제 -> 멘토링 및 리팩토링 필요 diff --git a/src/test/java/com/example/mate/common/utils/file/FileValidatorTest.java b/src/test/java/com/example/mate/domain/file/FileValidatorTest.java similarity index 97% rename from src/test/java/com/example/mate/common/utils/file/FileValidatorTest.java rename to src/test/java/com/example/mate/domain/file/FileValidatorTest.java index 36720d1f..e5efd2ea 100644 --- a/src/test/java/com/example/mate/common/utils/file/FileValidatorTest.java +++ b/src/test/java/com/example/mate/domain/file/FileValidatorTest.java @@ -1,8 +1,7 @@ -package com.example.mate.common.utils.file; +package com.example.mate.domain.file; import com.example.mate.common.error.CustomException; import com.example.mate.common.error.ErrorCode; -import com.example.mate.domain.file.FileValidator; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/src/test/java/com/example/mate/domain/goodsChat/controller/GoodsChatRoomControllerTest.java b/src/test/java/com/example/mate/domain/goodsChat/controller/GoodsChatRoomControllerTest.java index 876c5c83..366e3034 100644 --- a/src/test/java/com/example/mate/domain/goodsChat/controller/GoodsChatRoomControllerTest.java +++ b/src/test/java/com/example/mate/domain/goodsChat/controller/GoodsChatRoomControllerTest.java @@ -13,6 +13,8 @@ import com.example.mate.common.error.ErrorCode; import com.example.mate.common.response.PageResponse; import com.example.mate.common.security.util.JwtUtil; +import com.example.mate.common.security.filter.JwtCheckFilter; +import com.example.mate.common.security.util.JwtUtil; import com.example.mate.config.WithAuthMember; import com.example.mate.domain.goodsChat.dto.response.GoodsChatMessageResponse; import com.example.mate.domain.goodsChat.dto.response.GoodsChatRoomResponse; @@ -48,6 +50,9 @@ class GoodsChatRoomControllerTest { @MockBean private JwtUtil jwtUtil; + @MockBean + private JwtCheckFilter jwtCheckFilter; + @Test @DisplayName("굿즈거래 채팅방 생성 성공 - 기존 채팅방이 있을 경우 해당 채팅방을 반환한다.") void returnExistingChatRoom() throws Exception { diff --git a/src/test/java/com/example/mate/domain/mate/controller/MateControllerTest.java b/src/test/java/com/example/mate/domain/mate/controller/MateControllerTest.java index 7978227c..19c84865 100644 --- a/src/test/java/com/example/mate/domain/mate/controller/MateControllerTest.java +++ b/src/test/java/com/example/mate/domain/mate/controller/MateControllerTest.java @@ -1,10 +1,26 @@ package com.example.mate.domain.mate.controller; +import static com.example.mate.common.error.ErrorCode.ALREADY_COMPLETED_POST; +import static com.example.mate.common.error.ErrorCode.MATE_POST_NOT_FOUND_BY_ID; +import static com.example.mate.common.error.ErrorCode.MATE_POST_UPDATE_NOT_ALLOWED; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.example.mate.common.error.CustomException; import com.example.mate.common.error.ErrorCode; import com.example.mate.common.response.PageResponse; import com.example.mate.common.security.util.JwtUtil; -import com.example.mate.config.WithAuthMember; import com.example.mate.domain.constant.Gender; import com.example.mate.domain.mate.dto.request.MatePostCreateRequest; import com.example.mate.domain.mate.dto.request.MatePostUpdateRequest; @@ -15,14 +31,15 @@ import com.example.mate.domain.mate.entity.Status; import com.example.mate.domain.mate.entity.TransportType; import com.example.mate.domain.mate.service.MateService; -import com.example.mate.domain.member.service.LogoutRedisService; import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.LocalDateTime; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; import org.springframework.http.HttpMethod; @@ -31,22 +48,10 @@ import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import java.time.LocalDateTime; -import java.util.List; - -import static com.example.mate.common.error.ErrorCode.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@WebMvcTest(MateController.class) +//@WebMvcTest(MateController.class) +@SpringBootTest @MockBean(JpaMetamodelMappingContext.class) @AutoConfigureMockMvc(addFilters = false) -@WithAuthMember class MateControllerTest { @Autowired @@ -61,9 +66,6 @@ class MateControllerTest { @MockBean private JwtUtil jwtUtil; - @MockBean - private LogoutRedisService logoutRedisService; - private MatePostSummaryResponse createMatePostSummaryResponse() { return MatePostSummaryResponse.builder() .imageUrl("test-image.jpg") @@ -85,6 +87,7 @@ class CreateMatePost { private MatePostCreateRequest createMatePostRequest() { return MatePostCreateRequest.builder() + .memberId(1L) .teamId(1L) .matchId(1L) .title("테스트 제목") @@ -124,7 +127,7 @@ void createMatePost_success() throws Exception { "test image content".getBytes() ); - given(mateService.createMatePost(any(MatePostCreateRequest.class), any(), any())) + given(mateService.createMatePost(any(MatePostCreateRequest.class), any())) .willReturn(response); // when & then @@ -143,7 +146,6 @@ void createMatePost_success() throws Exception { @DisplayName("메이트 게시글 작성 성공 - 이미지 없음") void createMatePost_successWithoutImage() throws Exception { // given - Long memberId = 1L; MatePostCreateRequest request = createMatePostRequest(); MatePostResponse response = createMatePostResponse(); @@ -154,7 +156,7 @@ void createMatePost_successWithoutImage() throws Exception { objectMapper.writeValueAsBytes(request) ); - given(mateService.createMatePost(any(MatePostCreateRequest.class), any(), any())) + given(mateService.createMatePost(any(MatePostCreateRequest.class), any())) .willReturn(response); // when & then @@ -466,6 +468,7 @@ private MatePostResponse createMatePostResponse() { @DisplayName("메이트 게시글 수정 성공") void updateMatePost_Success() throws Exception { // given + Long memberId = 1L; Long postId = 1L; MatePostUpdateRequest request = createMatePostUpdateRequest(); MatePostResponse response = createMatePostResponse(); @@ -484,12 +487,12 @@ void updateMatePost_Success() throws Exception { "test image content".getBytes() ); - given(mateService.updateMatePost(any(), eq(postId), any(MatePostUpdateRequest.class), any())) + given(mateService.updateMatePost(eq(memberId), eq(postId), any(MatePostUpdateRequest.class), any())) .willReturn(response); // when & then mockMvc.perform(MockMvcRequestBuilders - .multipart(HttpMethod.PUT, "/api/mates/{postId}", postId) + .multipart(HttpMethod.PATCH, "/api/mates/{memberId}/{postId}", memberId, postId) .file(file) .file(data)) .andDo(print()) @@ -499,13 +502,14 @@ void updateMatePost_Success() throws Exception { .andExpect(jsonPath("$.data.status").value("모집중")) .andExpect(jsonPath("$.code").value(200)); - verify(mateService).updateMatePost(any(), eq(postId), any(MatePostUpdateRequest.class), any()); + verify(mateService).updateMatePost(eq(memberId), eq(postId), any(MatePostUpdateRequest.class), any()); } @Test @DisplayName("메이트 게시글 수정 성공 - 이미지 없음") void updateMatePost_SuccessWithoutImage() throws Exception { // given + Long memberId = 1L; Long postId = 1L; MatePostUpdateRequest request = createMatePostUpdateRequest(); MatePostResponse response = createMatePostResponse(); @@ -517,12 +521,12 @@ void updateMatePost_SuccessWithoutImage() throws Exception { objectMapper.writeValueAsBytes(request) ); - given(mateService.updateMatePost(any(), eq(postId), any(MatePostUpdateRequest.class), isNull())) + given(mateService.updateMatePost(eq(memberId), eq(postId), any(MatePostUpdateRequest.class), isNull())) .willReturn(response); // when & then mockMvc.perform(MockMvcRequestBuilders - .multipart(HttpMethod.PUT, "/api/mates/{postId}", postId) + .multipart(HttpMethod.PATCH, "/api/mates/{memberId}/{postId}", memberId, postId) .file(data)) .andDo(print()) .andExpect(status().isOk()) @@ -531,13 +535,14 @@ void updateMatePost_SuccessWithoutImage() throws Exception { .andExpect(jsonPath("$.data.status").value("모집중")) .andExpect(jsonPath("$.code").value(200)); - verify(mateService).updateMatePost(any(), eq(postId), any(MatePostUpdateRequest.class), isNull()); + verify(mateService).updateMatePost(eq(memberId), eq(postId), any(MatePostUpdateRequest.class), isNull()); } @Test @DisplayName("메이트 게시글 수정 실패 - 유효하지 않은 요청 데이터") void updateMatePost_FailWithInvalidRequest() throws Exception { // given + Long memberId = 1L; Long postId = 1L; MatePostUpdateRequest request = MatePostUpdateRequest.builder() .teamId(null) // 필수 값 누락 @@ -559,7 +564,7 @@ void updateMatePost_FailWithInvalidRequest() throws Exception { // when & then mockMvc.perform(MockMvcRequestBuilders - .multipart(HttpMethod.PUT, "/api/mates/{postId}", postId) + .multipart(HttpMethod.PATCH, "/api/mates/{memberId}/{postId}", memberId, postId) .file(data)) .andDo(print()) .andExpect(status().isBadRequest()); @@ -571,6 +576,7 @@ void updateMatePost_FailWithInvalidRequest() throws Exception { @DisplayName("메이트 게시글 수정 실패 - 존재하지 않는 게시글") void updateMatePost_FailWithPostNotFound() throws Exception { // given + Long memberId = 1L; Long postId = 999L; MatePostUpdateRequest request = createMatePostUpdateRequest(); @@ -586,7 +592,7 @@ void updateMatePost_FailWithPostNotFound() throws Exception { // when & then mockMvc.perform(MockMvcRequestBuilders - .multipart(HttpMethod.PUT, "/api/mates/{postId}", postId) + .multipart(HttpMethod.PATCH, "/api/mates/{memberId}/{postId}", memberId, postId) .file(data)) .andDo(print()) .andExpect(status().isNotFound()) @@ -598,6 +604,7 @@ void updateMatePost_FailWithPostNotFound() throws Exception { @DisplayName("메이트 게시글 수정 실패 - 권한 없음") void updateMatePost_FailWithUnauthorized() throws Exception { // given + Long memberId = 999L; Long postId = 1L; MatePostUpdateRequest request = createMatePostUpdateRequest(); @@ -613,7 +620,7 @@ void updateMatePost_FailWithUnauthorized() throws Exception { // when & then mockMvc.perform(MockMvcRequestBuilders - .multipart(HttpMethod.PUT, "/api/mates/{postId}", postId) + .multipart(HttpMethod.PATCH, "/api/mates/{memberId}/{postId}", memberId, postId) .file(data)) .andDo(print()) .andExpect(status().isForbidden()) @@ -625,6 +632,7 @@ void updateMatePost_FailWithUnauthorized() throws Exception { @DisplayName("메이트 게시글 수정 실패 - 이미 완료된 게시글") void updateMatePost_FailWithCompletedPost() throws Exception { // given + Long memberId = 1L; Long postId = 1L; MatePostUpdateRequest request = createMatePostUpdateRequest(); @@ -640,7 +648,7 @@ void updateMatePost_FailWithCompletedPost() throws Exception { // when & then mockMvc.perform(MockMvcRequestBuilders - .multipart(HttpMethod.PUT, "/api/mates/{postId}", postId) + .multipart(HttpMethod.PATCH, "/api/mates/{memberId}/{postId}", memberId, postId) .file(data)) .andDo(print()) .andExpect(status().isForbidden()) @@ -661,7 +669,7 @@ void deleteMatePost_success() throws Exception { Long postId = 1L; // when & then - mockMvc.perform(delete("/api/mates/{postId}", postId) + mockMvc.perform(delete("/api/mates/{memberId}/{postId}", memberId, postId) .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isNoContent()); @@ -681,7 +689,7 @@ void deleteMatePost_failPostNotFound() throws Exception { .deleteMatePost(memberId, nonExistentPostId); // when & then - mockMvc.perform(delete("/api/mates/{postId}", nonExistentPostId) + mockMvc.perform(delete("/api/mates/{memberId}/{postId}", memberId, nonExistentPostId) .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isNotFound()) @@ -692,7 +700,6 @@ void deleteMatePost_failPostNotFound() throws Exception { @Test @DisplayName("메이트 게시글 삭제 실패 - 삭제 권한 없음") - @WithAuthMember(memberId = 2L) void deleteMatePost_failNotAllowed() throws Exception { // given Long memberId = 2L; // 작성자가 아닌 다른 사용자 @@ -703,7 +710,7 @@ void deleteMatePost_failNotAllowed() throws Exception { .deleteMatePost(memberId, postId); // when & then - mockMvc.perform(delete("/api/mates/{postId}", postId) + mockMvc.perform(delete("/api/mates/{memberId}/{postId}", memberId, postId) .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isForbidden()) diff --git a/src/test/java/com/example/mate/domain/mate/controller/MateReviewControllerTest.java b/src/test/java/com/example/mate/domain/mate/controller/MateReviewControllerTest.java index 0809a45a..f6bb84ab 100644 --- a/src/test/java/com/example/mate/domain/mate/controller/MateReviewControllerTest.java +++ b/src/test/java/com/example/mate/domain/mate/controller/MateReviewControllerTest.java @@ -1,7 +1,13 @@ package com.example.mate.domain.mate.controller; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.example.mate.common.security.filter.JwtCheckFilter; -import com.example.mate.config.WithAuthMember; import com.example.mate.domain.constant.Rating; import com.example.mate.domain.mate.dto.request.MateReviewCreateRequest; import com.example.mate.domain.mate.dto.response.MateReviewCreateResponse; @@ -18,17 +24,9 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @WebMvcTest(MateController.class) @MockBean(JpaMetamodelMappingContext.class) @AutoConfigureMockMvc(addFilters = false) -@WithAuthMember class MateReviewControllerTest { @Autowired @@ -70,6 +68,7 @@ private MateReviewCreateResponse createMateReviewResponse() { @DisplayName("메이트 직관 후기 작성 성공") void createMateReview_success() throws Exception { // given + Long memberId = 1L; Long postId = 1L; MateReviewCreateRequest request = createMateReviewRequest(); MateReviewCreateResponse response = createMateReviewResponse(); @@ -78,7 +77,7 @@ void createMateReview_success() throws Exception { .willReturn(response); // when & then - mockMvc.perform(post("/api/mates/{postId}/reviews", postId) + mockMvc.perform(post("/api/mates/{memberId}/{postId}/reviews", memberId, postId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) @@ -97,6 +96,7 @@ void createMateReview_success() throws Exception { @DisplayName("메이트 직관 후기 작성 실패 - 리뷰 대상자 ID 누락") void createMateReview_failWithoutRevieweeId() throws Exception { // given + Long memberId = 1L; Long postId = 1L; MateReviewCreateRequest request = MateReviewCreateRequest.builder() .rating(Rating.GOOD) @@ -104,7 +104,7 @@ void createMateReview_failWithoutRevieweeId() throws Exception { .build(); // when & then - mockMvc.perform(post("/api/mates/{postId}/reviews", postId) + mockMvc.perform(post("/api/mates/{memberId}/{postId}/reviews", memberId, postId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) @@ -128,7 +128,7 @@ void createMateReview_failWithLongContent() throws Exception { .build(); // when & then - mockMvc.perform(post("/api/mates/{postId}/reviews", memberId, postId) + mockMvc.perform(post("/api/mates/{memberId}/{postId}/reviews", memberId, postId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) diff --git a/src/test/java/com/example/mate/domain/mate/controller/MateStatusControllerTest.java b/src/test/java/com/example/mate/domain/mate/controller/MateStatusControllerTest.java index f1f282cb..33be6016 100644 --- a/src/test/java/com/example/mate/domain/mate/controller/MateStatusControllerTest.java +++ b/src/test/java/com/example/mate/domain/mate/controller/MateStatusControllerTest.java @@ -1,8 +1,25 @@ package com.example.mate.domain.mate.controller; +import static com.example.mate.common.error.ErrorCode.ALREADY_COMPLETED_POST; +import static com.example.mate.common.error.ErrorCode.INVALID_MATE_POST_PARTICIPANT_IDS; +import static com.example.mate.common.error.ErrorCode.MATE_POST_COMPLETE_TIME_NOT_ALLOWED; +import static com.example.mate.common.error.ErrorCode.MATE_POST_MAX_PARTICIPANTS_EXCEEDED; +import static com.example.mate.common.error.ErrorCode.MATE_POST_NOT_FOUND_BY_ID; +import static com.example.mate.common.error.ErrorCode.MATE_POST_UPDATE_NOT_ALLOWED; +import static com.example.mate.common.error.ErrorCode.NOT_CLOSED_STATUS_FOR_COMPLETION; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.example.mate.common.error.CustomException; +import com.example.mate.common.error.ErrorCode; import com.example.mate.common.security.filter.JwtCheckFilter; -import com.example.mate.config.WithAuthMember; import com.example.mate.domain.mate.dto.request.MatePostCompleteRequest; import com.example.mate.domain.mate.dto.request.MatePostStatusRequest; import com.example.mate.domain.mate.dto.response.MatePostCompleteResponse; @@ -10,6 +27,7 @@ import com.example.mate.domain.mate.entity.Status; import com.example.mate.domain.mate.service.MateService; import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -21,23 +39,9 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import java.util.List; - -import static com.example.mate.common.error.ErrorCode.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @WebMvcTest(MateController.class) @MockBean(JpaMetamodelMappingContext.class) @AutoConfigureMockMvc(addFilters = false) -@WithAuthMember class MateStatusControllerTest { @Autowired @@ -60,6 +64,7 @@ class UpdateMatePostStatus { @DisplayName("메이트 게시글 상태 변경 성공 - OPEN 상태로 변경") void updateMatePostStatus_successToOpen() throws Exception { // given + Long memberId = 1L; Long postId = 1L; List<Long> participantIds = List.of(2L, 3L); MatePostStatusRequest request = new MatePostStatusRequest(Status.OPEN, participantIds); @@ -68,11 +73,11 @@ void updateMatePostStatus_successToOpen() throws Exception { .status(Status.OPEN) .build(); - given(mateService.updateMatePostStatus(any(), eq(postId), any(MatePostStatusRequest.class))) + given(mateService.updateMatePostStatus(eq(memberId), eq(postId), any(MatePostStatusRequest.class))) .willReturn(response); // when & then - mockMvc.perform(patch("/api/mates/{postId}/status", postId) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/status", memberId, postId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) @@ -82,13 +87,14 @@ void updateMatePostStatus_successToOpen() throws Exception { .andExpect(jsonPath("$.data.status").value("모집중")) .andExpect(jsonPath("$.code").value(200)); - verify(mateService).updateMatePostStatus(any(), eq(postId), any(MatePostStatusRequest.class)); + verify(mateService).updateMatePostStatus(eq(memberId), eq(postId), any(MatePostStatusRequest.class)); } @Test @DisplayName("메이트 게시글 상태 변경 성공 - CLOSED 상태로 변경") void updateMatePostStatus_successToClosed() throws Exception { // given + Long memberId = 1L; Long postId = 1L; List<Long> participantIds = List.of(2L, 3L, 4L); MatePostStatusRequest request = new MatePostStatusRequest(Status.CLOSED, participantIds); @@ -97,11 +103,11 @@ void updateMatePostStatus_successToClosed() throws Exception { .status(Status.CLOSED) .build(); - given(mateService.updateMatePostStatus(any(), eq(postId), any(MatePostStatusRequest.class))) + given(mateService.updateMatePostStatus(eq(memberId), eq(postId), any(MatePostStatusRequest.class))) .willReturn(response); // when & then - mockMvc.perform(patch("/api/mates/{postId}/status", postId) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/status", memberId, postId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) @@ -111,23 +117,24 @@ void updateMatePostStatus_successToClosed() throws Exception { .andExpect(jsonPath("$.data.status").value("모집완료")) .andExpect(jsonPath("$.code").value(200)); - verify(mateService).updateMatePostStatus(any(), eq(postId), any(MatePostStatusRequest.class)); + verify(mateService).updateMatePostStatus(eq(memberId), eq(postId), any(MatePostStatusRequest.class)); } @Test @DisplayName("메이트 게시글 상태 변경 실패 - 존재하지 않는 게시글") void updateMatePostStatus_failPostNotFound() throws Exception { // given + Long memberId = 1L; Long nonExistentPostId = 999L; List<Long> participantIds = List.of(2L, 3L); MatePostStatusRequest request = new MatePostStatusRequest(Status.CLOSED, participantIds); - given(mateService.updateMatePostStatus(any(), eq(nonExistentPostId), + given(mateService.updateMatePostStatus(eq(memberId), eq(nonExistentPostId), any(MatePostStatusRequest.class))) .willThrow(new CustomException(MATE_POST_NOT_FOUND_BY_ID)); // when & then - mockMvc.perform(patch("/api/mates/{postId}/status", nonExistentPostId) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/status", memberId, nonExistentPostId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) @@ -136,23 +143,49 @@ void updateMatePostStatus_failPostNotFound() throws Exception { .andExpect(jsonPath("$.message").exists()) .andExpect(jsonPath("$.code").value(404)); - verify(mateService).updateMatePostStatus(any(), eq(nonExistentPostId), + verify(mateService).updateMatePostStatus(eq(memberId), eq(nonExistentPostId), any(MatePostStatusRequest.class)); } + @Test + @DisplayName("메이트 게시글 상태 변경 실패 - 권한 없음") + void updateMatePostStatus_failNotAuthorized() throws Exception { + // given + Long memberId = 2L; + Long postId = 1L; + List<Long> participantIds = List.of(2L, 3L); + MatePostStatusRequest request = new MatePostStatusRequest(Status.CLOSED, participantIds); + + given(mateService.updateMatePostStatus(eq(memberId), eq(postId), any(MatePostStatusRequest.class))) + .willThrow(new CustomException(ErrorCode.MATE_POST_UPDATE_NOT_ALLOWED)); + + // when & then + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/status", memberId, postId) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.status").value("ERROR")) + .andExpect(jsonPath("$.message").exists()) + .andExpect(jsonPath("$.code").value(403)); + + verify(mateService).updateMatePostStatus(eq(memberId), eq(postId), any(MatePostStatusRequest.class)); + } + @Test @DisplayName("메이트 게시글 상태 변경 실패 - VISIT_COMPLETE로 변경 시도") void updateMatePostStatus_failWithCompleteStatus() throws Exception { // given + Long memberId = 1L; Long postId = 1L; List<Long> participantIds = List.of(2L, 3L); MatePostStatusRequest request = new MatePostStatusRequest(Status.VISIT_COMPLETE, participantIds); - given(mateService.updateMatePostStatus(any(), eq(postId), any(MatePostStatusRequest.class))) + given(mateService.updateMatePostStatus(eq(memberId), eq(postId), any(MatePostStatusRequest.class))) .willThrow(new CustomException(ALREADY_COMPLETED_POST)); // when & then - mockMvc.perform(patch("/api/mates/{postId}/status", postId) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/status", memberId, postId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) @@ -161,22 +194,23 @@ void updateMatePostStatus_failWithCompleteStatus() throws Exception { .andExpect(jsonPath("$.message").exists()) .andExpect(jsonPath("$.code").value(403)); - verify(mateService).updateMatePostStatus(any(), eq(postId), any(MatePostStatusRequest.class)); + verify(mateService).updateMatePostStatus(eq(memberId), eq(postId), any(MatePostStatusRequest.class)); } @Test @DisplayName("메이트 게시글 상태 변경 실패 - 이미 완료된 게시글") void updateMatePostStatus_failAlreadyCompleted() throws Exception { // given + Long memberId = 1L; Long postId = 1L; List<Long> participantIds = List.of(2L, 3L); MatePostStatusRequest request = new MatePostStatusRequest(Status.CLOSED, participantIds); - given(mateService.updateMatePostStatus(any(), eq(postId), any(MatePostStatusRequest.class))) + given(mateService.updateMatePostStatus(eq(memberId), eq(postId), any(MatePostStatusRequest.class))) .willThrow(new CustomException(ALREADY_COMPLETED_POST)); // when & then - mockMvc.perform(patch("/api/mates/{postId}/status", postId) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/status", memberId, postId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) @@ -185,23 +219,24 @@ void updateMatePostStatus_failAlreadyCompleted() throws Exception { .andExpect(jsonPath("$.message").exists()) .andExpect(jsonPath("$.code").value(403)); - verify(mateService).updateMatePostStatus(any(), eq(postId), any(MatePostStatusRequest.class)); + verify(mateService).updateMatePostStatus(eq(memberId), eq(postId), any(MatePostStatusRequest.class)); } @Test @DisplayName("메이트 게시글 상태 변경 실패 - 참여자 수 초과") void updateMatePostStatus_failMaxParticipantsExceeded() throws Exception { // given + Long memberId = 1L; Long postId = 1L; // @Size(max = 9) 제약조건을 통과하도록 9명으로 수정 List<Long> participantIds = List.of(2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L); MatePostStatusRequest request = new MatePostStatusRequest(Status.CLOSED, participantIds); - given(mateService.updateMatePostStatus(any(), eq(postId), any(MatePostStatusRequest.class))) + given(mateService.updateMatePostStatus(eq(memberId), eq(postId), any(MatePostStatusRequest.class))) .willThrow(new CustomException(MATE_POST_MAX_PARTICIPANTS_EXCEEDED)); // when & then - mockMvc.perform(patch("/api/mates/{postId}/status", postId) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/status", memberId, postId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) @@ -210,20 +245,21 @@ void updateMatePostStatus_failMaxParticipantsExceeded() throws Exception { .andExpect(jsonPath("$.message").value(MATE_POST_MAX_PARTICIPANTS_EXCEEDED.getMessage())) .andExpect(jsonPath("$.code").value(400)); - verify(mateService).updateMatePostStatus(any(), eq(postId), any(MatePostStatusRequest.class)); + verify(mateService).updateMatePostStatus(eq(memberId), eq(postId), any(MatePostStatusRequest.class)); } @Test @DisplayName("메이트 게시글 상태 변경 실패 - 참여자 수 validation 실패") void updateMatePostStatus_failMaxParticipantsValidation() throws Exception { // given + Long memberId = 1L; Long postId = 1L; // @Size(max = 9) 제약조건을 초과하는 10명 List<Long> participantIds = List.of(2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L); MatePostStatusRequest request = new MatePostStatusRequest(Status.CLOSED, participantIds); // when & then - mockMvc.perform(patch("/api/mates/{postId}/status", postId) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/status", memberId, postId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) @@ -240,15 +276,16 @@ void updateMatePostStatus_failMaxParticipantsValidation() throws Exception { @DisplayName("메이트 게시글 상태 변경 실패 - 잘못된 참여자 ID") void updateMatePostStatus_failInvalidParticipantIds() throws Exception { // given + Long memberId = 1L; Long postId = 1L; List<Long> participantIds = List.of(999L, 998L); // 존재하지 않는 참여자 ID MatePostStatusRequest request = new MatePostStatusRequest(Status.CLOSED, participantIds); - given(mateService.updateMatePostStatus(any(), eq(postId), any(MatePostStatusRequest.class))) + given(mateService.updateMatePostStatus(eq(memberId), eq(postId), any(MatePostStatusRequest.class))) .willThrow(new CustomException(INVALID_MATE_POST_PARTICIPANT_IDS)); // when & then - mockMvc.perform(patch("/api/mates/{postId}/status", postId) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/status", memberId, postId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) @@ -257,7 +294,7 @@ void updateMatePostStatus_failInvalidParticipantIds() throws Exception { .andExpect(jsonPath("$.message").exists()) .andExpect(jsonPath("$.code").value(400)); - verify(mateService).updateMatePostStatus(any(), eq(postId), any(MatePostStatusRequest.class)); + verify(mateService).updateMatePostStatus(eq(memberId), eq(postId), any(MatePostStatusRequest.class)); } } @@ -290,7 +327,7 @@ void completeVisit_success() throws Exception { .willReturn(response); // when & then - mockMvc.perform(patch("/api/mates/{postId}/complete", POST_ID) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/complete", MEMBER_ID, POST_ID) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) @@ -315,7 +352,7 @@ void completeVisit_failPostNotFound() throws Exception { .willThrow(new CustomException(MATE_POST_NOT_FOUND_BY_ID)); // when & then - mockMvc.perform(patch("/api/mates/{postId}/complete", POST_ID) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/complete", MEMBER_ID, POST_ID) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) @@ -335,7 +372,7 @@ void completeVisit_failNotAuthorized() throws Exception { .willThrow(new CustomException(MATE_POST_UPDATE_NOT_ALLOWED)); // when & then - mockMvc.perform(patch("/api/mates/{postId}/complete", POST_ID) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/complete", MEMBER_ID, POST_ID) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) @@ -355,7 +392,7 @@ void completeVisit_failBeforeMatchTime() throws Exception { .willThrow(new CustomException(MATE_POST_COMPLETE_TIME_NOT_ALLOWED)); // when & then - mockMvc.perform(patch("/api/mates/{postId}/complete", POST_ID) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/complete", MEMBER_ID, POST_ID) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) @@ -375,7 +412,7 @@ void completeVisit_failNotClosedStatus() throws Exception { .willThrow(new CustomException(NOT_CLOSED_STATUS_FOR_COMPLETION)); // when & then - mockMvc.perform(patch("/api/mates/{postId}/complete", POST_ID) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/complete", MEMBER_ID, POST_ID) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) @@ -395,7 +432,7 @@ void completeVisit_failExceededParticipants() throws Exception { .willThrow(new CustomException(MATE_POST_MAX_PARTICIPANTS_EXCEEDED)); // when & then - mockMvc.perform(patch("/api/mates/{postId}/complete", POST_ID) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/complete", MEMBER_ID, POST_ID) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andDo(print()) diff --git a/src/test/java/com/example/mate/domain/mate/integration/MateIntegrationTest.java b/src/test/java/com/example/mate/domain/mate/integration/MateIntegrationTest.java index 8f3d4a66..12a13a7c 100644 --- a/src/test/java/com/example/mate/domain/mate/integration/MateIntegrationTest.java +++ b/src/test/java/com/example/mate/domain/mate/integration/MateIntegrationTest.java @@ -1,7 +1,6 @@ package com.example.mate.domain.mate.integration; import com.example.mate.common.security.util.JwtUtil; -import com.example.mate.config.WithAuthMember; import com.example.mate.domain.constant.Gender; import com.example.mate.domain.match.entity.Match; import com.example.mate.domain.match.repository.MatchRepository; @@ -25,7 +24,6 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageRequest; import org.springframework.http.MediaType; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; @@ -62,9 +60,6 @@ public class MateIntegrationTest { @Autowired private MateRepository mateRepository; - @Autowired - private JdbcTemplate jdbcTemplate; - @MockBean private JwtUtil jwtUtil; @@ -83,8 +78,6 @@ void setUp() { matchRepository.deleteAll(); memberRepository.deleteAll(); - jdbcTemplate.execute("ALTER TABLE member ALTER COLUMN id RESTART WITH 1"); - // 테스트 멤버 생성 testMember = createTestMember(); @@ -158,10 +151,10 @@ class CreateMatePost { @Test @DisplayName("메이트 게시글 작성 성공") - @WithAuthMember void createMatePost_Success() throws Exception { // given MatePostCreateRequest request = MatePostCreateRequest.builder() + .memberId(testMember.getId()) .teamId(1L) .matchId(futureMatch.getId()) .title("통합 테스트 제목") @@ -205,11 +198,36 @@ void createMatePost_Success() throws Exception { assertThat(savedPost.getTransport()).isEqualTo(request.getTransportType()); } + @Test + @DisplayName("존재하지 않는 회원으로 메이트 게시글 작성 시 실패") + void createMatePost_WithInvalidMember() throws Exception { + MatePostCreateRequest request = MatePostCreateRequest.builder() + .memberId(999L) + .teamId(1L) + .matchId(futureMatch.getId()) + .title("통합 테스트 제목") + .content("통합 테스트 내용") + .age(Age.TWENTIES) + .maxParticipants(4) + .gender(Gender.FEMALE) + .transportType(TransportType.PUBLIC) + .build(); + + MockMultipartFile data = new MockMultipartFile( + "data", + "", + MediaType.APPLICATION_JSON_VALUE, + objectMapper.writeValueAsBytes(request) + ); + + performErrorTest(data, MEMBER_NOT_FOUND_BY_ID.getMessage(), 404); + } + @Test @DisplayName("존재하지 않는 경기로 메이트 게시글 작성 시 실패") - @WithAuthMember void createMatePost_WithInvalidMatch() throws Exception { MatePostCreateRequest request = MatePostCreateRequest.builder() + .memberId(testMember.getId()) .teamId(1L) .matchId(999L) .title("통합 테스트 제목") @@ -566,10 +584,9 @@ class DeleteMatePost { @Test @DisplayName("메이트 게시글 삭제 성공") - @WithAuthMember void deleteMatePost_Success() throws Exception { // when & then - mockMvc.perform(delete("/api/mates/{postId}", openPost.getId()) + mockMvc.perform(delete("/api/mates/{memberId}/{postId}", testMember.getId(), openPost.getId()) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNoContent()) .andDo(print()); @@ -580,10 +597,9 @@ void deleteMatePost_Success() throws Exception { @Test @DisplayName("메이트 게시글 삭제 실패 - 존재하지 않는 게시글") - @WithAuthMember void deleteMatePost_NotFound() throws Exception { // when & then - mockMvc.perform(delete("/api/mates/{postId}", 999L) + mockMvc.perform(delete("/api/mates/{memberId}/{postId}", testMember.getId(), 999L) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.status").value("ERROR")) @@ -594,5 +610,51 @@ void deleteMatePost_NotFound() throws Exception { // DB 검증 - 기존 게시글들은 여전히 존재 assertThat(mateRepository.findAll()).hasSize(3); } + + @Test + @DisplayName("메이트 게시글 삭제 실패 - 권한 없음") + void deleteMatePost_NotAllowed() throws Exception { + // given + Member otherMember = memberRepository.save(Member.builder() + .name("다른유저") + .email("other@test.com") + .nickname("다른계정") + .imageUrl("other.jpg") + .gender(Gender.MALE) + .age(30) + .manner(0.3f) + .build()); + + // when & then + mockMvc.perform(delete("/api/mates/{memberId}/{postId}", otherMember.getId(), openPost.getId()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.status").value("ERROR")) + .andExpect(jsonPath("$.message").value(MATE_POST_UPDATE_NOT_ALLOWED.getMessage())) + .andExpect(jsonPath("$.code").value(403)) + .andDo(print()); + + // DB 검증 - 게시글이 삭제되지 않음 + assertThat(mateRepository.findById(openPost.getId())).isPresent(); + } +// +// @Test +// @DisplayName("직관 완료된 게시글 삭제 시 Visit 엔티티와 연관관계 제거") +// void deleteMatePost_WithCompletedStatus() throws Exception { +// // given +// MatePost post = createMatePost(futureMatch, 1L, Status.CLOSED); // CLOSED 상태로 생성 +// post.completeVisit(List.of(testMember.getId())); // completeVisit 호출하여 COMPLETE로 변경 +// Visit visit = post.getVisit(); +// +// // when +// mockMvc.perform(delete("/api/mates/{memberId}/{postId}", testMember.getId(), post.getId()) +// .contentType(MediaType.APPLICATION_JSON)) +// .andExpect(status().isNoContent()) +// .andDo(print()); +// +// // then +// assertThat(mateRepository.findById(post.getId())).isEmpty(); +// assertThat(visit.getPost()).isNull(); +// } } } \ No newline at end of file diff --git a/src/test/java/com/example/mate/domain/mate/integration/MateStatusIntegrationTest.java b/src/test/java/com/example/mate/domain/mate/integration/MateStatusIntegrationTest.java index 5e1a7488..a62c9faf 100644 --- a/src/test/java/com/example/mate/domain/mate/integration/MateStatusIntegrationTest.java +++ b/src/test/java/com/example/mate/domain/mate/integration/MateStatusIntegrationTest.java @@ -18,7 +18,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.example.mate.common.security.util.JwtUtil; -import com.example.mate.config.WithAuthMember; import com.example.mate.domain.constant.Gender; import com.example.mate.domain.match.entity.Match; import com.example.mate.domain.match.repository.MatchRepository; @@ -45,7 +44,6 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; -import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; @@ -53,7 +51,6 @@ @SpringBootTest @AutoConfigureMockMvc(addFilters = false) @Transactional -@WithAuthMember public class MateStatusIntegrationTest { @Autowired @@ -71,9 +68,6 @@ public class MateStatusIntegrationTest { @Autowired private MateRepository mateRepository; - @Autowired - private JdbcTemplate jdbcTemplate; - private Member testMember; private Member participant1; private Member participant2; @@ -93,8 +87,6 @@ void setUp() { matchRepository.deleteAll(); memberRepository.deleteAll(); - jdbcTemplate.execute("ALTER TABLE member ALTER COLUMN id RESTART WITH 1"); - // 테스트 멤버와 참여자들 생성 testMember = createTestMember("testMember"); participant1 = createTestMember("part1"); @@ -159,7 +151,7 @@ void updateMatePostStatus_OpenToClosed_Success() throws Exception { MatePostStatusRequest request = new MatePostStatusRequest(Status.CLOSED, participantIds); // when & then - mockMvc.perform(patch("/api/mates/{postId}/status", openPost.getId()) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/status", testMember.getId(), openPost.getId()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()) @@ -182,7 +174,7 @@ void updateMatePostStatus_ClosedToOpen_Success() throws Exception { MatePostStatusRequest request = new MatePostStatusRequest(Status.OPEN, participantIds); // when & then - mockMvc.perform(patch("/api/mates/{postId}/status", closedPost.getId()) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/status", testMember.getId(), closedPost.getId()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()) @@ -205,7 +197,7 @@ void updateMatePostStatus_ToComplete_Failure() throws Exception { MatePostStatusRequest request = new MatePostStatusRequest(Status.VISIT_COMPLETE, participantIds); // when & then - mockMvc.perform(patch("/api/mates/{postId}/status", openPost.getId()) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/status", testMember.getId(), openPost.getId()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isForbidden()) @@ -227,7 +219,7 @@ void updateMatePostStatus_AlreadyCompleted_Failure() throws Exception { MatePostStatusRequest request = new MatePostStatusRequest(Status.OPEN, participantIds); // when & then - mockMvc.perform(patch("/api/mates/{postId}/status", completedPost.getId()) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/status", testMember.getId(), completedPost.getId()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isForbidden()) @@ -241,6 +233,30 @@ void updateMatePostStatus_AlreadyCompleted_Failure() throws Exception { assertThat(unchangedPost.getStatus()).isEqualTo(Status.VISIT_COMPLETE); } + @Test + @DisplayName("게시글 작성자가 아닌 사용자가 상태 변경 시도시 실패") + void updateMatePostStatus_NotAuthor_Failure() throws Exception { + // given + Member otherMember = createTestMember("otherMem"); + List<Long> participantIds = Arrays.asList(participant1.getId(), participant2.getId()); + MatePostStatusRequest request = new MatePostStatusRequest(Status.CLOSED, participantIds); + + // when & then + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/status", otherMember.getId(), openPost.getId()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.status").value("ERROR")) + .andExpect(jsonPath("$.message").value(MATE_POST_UPDATE_NOT_ALLOWED.getMessage())) + .andExpect(jsonPath("$.code").value(403)) + .andDo(print()); + + // DB 검증 + MatePost unchangedPost = mateRepository.findById(openPost.getId()).orElseThrow(); + assertThat(unchangedPost.getStatus()).isEqualTo(Status.OPEN); + } + + @Test @DisplayName("존재하지 않는 게시글의 상태 변경 시도시 실패") void updateMatePostStatus_PostNotFound_Failure() throws Exception { @@ -249,7 +265,7 @@ void updateMatePostStatus_PostNotFound_Failure() throws Exception { MatePostStatusRequest request = new MatePostStatusRequest(Status.CLOSED, participantIds); // when & then - mockMvc.perform(patch("/api/mates/{postId}/status", 999L) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/status", testMember.getId(), 999L) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isNotFound()) @@ -272,7 +288,7 @@ void updateMatePostStatus_ExceedMaxParticipants_Failure() throws Exception { MatePostStatusRequest request = new MatePostStatusRequest(Status.CLOSED, participantIds); // when & then - mockMvc.perform(patch("/api/mates/{postId}/status", openPost.getId()) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/status", testMember.getId(), openPost.getId()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()) @@ -294,7 +310,7 @@ void updateMatePostStatus_InvalidParticipantId_Failure() throws Exception { MatePostStatusRequest request = new MatePostStatusRequest(Status.CLOSED, participantIds); // when & then - mockMvc.perform(patch("/api/mates/{postId}/status", openPost.getId()) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/status", testMember.getId(), openPost.getId()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()) @@ -322,8 +338,8 @@ void completeVisit_Success() throws Exception { ); // when & then - mockMvc.perform(patch("/api/mates/{postId}/complete", - closedPost.getId()) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/complete", + testMember.getId(), closedPost.getId()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsBytes(request))) .andExpect(status().isOk()) @@ -346,7 +362,6 @@ void completeVisit_Success() throws Exception { @Test @DisplayName("직관 완료 처리 실패 - 권한 없음") - @WithAuthMember(memberId = 2L) void completeVisit_Fail_NotAuthor() throws Exception { // given MatePostCompleteRequest request = new MatePostCompleteRequest( @@ -354,8 +369,8 @@ void completeVisit_Fail_NotAuthor() throws Exception { ); // when & then - mockMvc.perform(patch("/api/mates/{postId}/complete" - ,closedPost.getId()) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/complete", + participant1.getId(), closedPost.getId()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsBytes(request))) .andExpect(status().isForbidden()) @@ -380,8 +395,8 @@ void completeVisit_Fail_NotClosedStatus() throws Exception { ); // when & then - mockMvc.perform(patch("/api/mates/{postId}/complete", - openPost.getId()) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/complete", + testMember.getId(), openPost.getId()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsBytes(request))) .andExpect(status().isBadRequest()) @@ -406,8 +421,8 @@ void completeVisit_Fail_BeforeMatchTime() throws Exception { ); // when & then - mockMvc.perform(patch("/api/mates/{postId}/complete", - futureClosedPost.getId()) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/complete", + testMember.getId(), futureClosedPost.getId()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsBytes(request))) .andExpect(status().isForbidden()) @@ -432,8 +447,8 @@ void completeVisit_Fail_InvalidParticipant() throws Exception { ); // when & then - mockMvc.perform(patch("/api/mates/{postId}/complete", - closedPost.getId()) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/complete", + testMember.getId(), closedPost.getId()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsBytes(request))) .andExpect(status().isBadRequest()) @@ -458,8 +473,8 @@ void completeVisit_Fail_ExceedMaxParticipants() throws Exception { ); // when & then - mockMvc.perform(patch("/api/mates/{postId}/complete", - closedPost.getId()) + mockMvc.perform(patch("/api/mates/{memberId}/{postId}/complete", + testMember.getId(), closedPost.getId()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsBytes(request))) .andExpect(status().isBadRequest()) diff --git a/src/test/java/com/example/mate/domain/mate/service/MateServiceTest.java b/src/test/java/com/example/mate/domain/mate/service/MateServiceTest.java index 8eb5c642..e0df0e6d 100644 --- a/src/test/java/com/example/mate/domain/mate/service/MateServiceTest.java +++ b/src/test/java/com/example/mate/domain/mate/service/MateServiceTest.java @@ -95,6 +95,7 @@ void createMatePost_Success() { Match testMatch = createTestMatch(); MatePostCreateRequest request = MatePostCreateRequest.builder() + .memberId(TEST_MEMBER_ID) .teamId(TEST_MATCH_ID) .matchId(1L) .title("테스트 제목") @@ -119,7 +120,7 @@ void createMatePost_Success() { .transport(TransportType.PUBLIC) .build(); - given(memberRepository.findById(TEST_MEMBER_ID)) + given(memberRepository.findById(request.getMemberId())) .willReturn(Optional.of(testMember)); given(matchRepository.findById(request.getMatchId())) .willReturn(Optional.of(testMatch)); @@ -127,7 +128,7 @@ void createMatePost_Success() { .willReturn(matePost); // when - MatePostResponse response = mateService.createMatePost(request, null, TEST_MEMBER_ID); + MatePostResponse response = mateService.createMatePost(request, null); // then assertThat(response.getStatus()).isEqualTo(Status.OPEN); @@ -141,6 +142,7 @@ void createMatePost_Success() { void createMatePost_FailWithInvalidMember() { // given MatePostCreateRequest request = MatePostCreateRequest.builder() + .memberId(TEST_MEMBER_ID) .teamId(TEST_MATCH_ID) .matchId(1L) .title("테스트 제목") @@ -151,11 +153,11 @@ void createMatePost_FailWithInvalidMember() { .transportType(TransportType.PUBLIC) .build(); - given(memberRepository.findById(TEST_MEMBER_ID)) + given(memberRepository.findById(request.getMemberId())) .willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> mateService.createMatePost(request, null, TEST_MEMBER_ID)) + assertThatThrownBy(() -> mateService.createMatePost(request, null)) .isInstanceOf(CustomException.class) .hasFieldOrPropertyWithValue("errorCode", MEMBER_NOT_FOUND_BY_ID); @@ -170,6 +172,7 @@ void createMatePost_FailWithInvalidMatch() { // given Member testMember = createTestMember(); MatePostCreateRequest request = MatePostCreateRequest.builder() + .memberId(TEST_MEMBER_ID) .teamId(TEST_MATCH_ID) .matchId(1L) .title("테스트 제목") @@ -180,13 +183,13 @@ void createMatePost_FailWithInvalidMatch() { .transportType(TransportType.PUBLIC) .build(); - given(memberRepository.findById(TEST_MEMBER_ID)) + given(memberRepository.findById(request.getMemberId())) .willReturn(Optional.of(testMember)); given(matchRepository.findById(request.getMatchId())) .willReturn(Optional.empty()); // when & then - assertThatThrownBy(() -> mateService.createMatePost(request, null, TEST_MEMBER_ID)) + assertThatThrownBy(() -> mateService.createMatePost(request, null)) .isInstanceOf(CustomException.class) .hasFieldOrPropertyWithValue("errorCode", MATCH_NOT_FOUND_BY_ID); diff --git a/src/test/java/com/example/mate/domain/member/controller/MemberControllerTest.java b/src/test/java/com/example/mate/domain/member/controller/MemberControllerTest.java index 2535f5cc..29d915f3 100644 --- a/src/test/java/com/example/mate/domain/member/controller/MemberControllerTest.java +++ b/src/test/java/com/example/mate/domain/member/controller/MemberControllerTest.java @@ -11,6 +11,7 @@ import com.example.mate.domain.member.dto.response.MemberLoginResponse; import com.example.mate.domain.member.dto.response.MemberProfileResponse; import com.example.mate.domain.member.dto.response.MyProfileResponse; +import com.example.mate.domain.member.service.LogoutRedisService; import com.example.mate.domain.member.service.MemberService; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.DisplayName; @@ -21,6 +22,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.mock.web.MockMultipartFile; @@ -52,6 +54,9 @@ class MemberControllerTest { @MockBean private JwtCheckFilter jwtCheckFilter; + @MockBean + private LogoutRedisService logoutRedisService; + private MyProfileResponse createMyProfileResponse() { return MyProfileResponse.builder() .nickname("tester") @@ -419,4 +424,41 @@ void login_member_success() throws Exception { .andDo(print()); } } + + @Nested + @DisplayName("회원 로그아웃") + class LogoutMember { + + @Test + @DisplayName("회원 로그아웃 성공") + void logout_member_success() throws Exception { + // given + String token = "Bearer accessToken"; + + doNothing().when(logoutRedisService).addTokenToBlacklist(anyString()); + + // when & then + mockMvc.perform(post("/api/members/logout") + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isNoContent()); + + verify(logoutRedisService).addTokenToBlacklist(token); + } + + @Test + @DisplayName("로그아웃 실패 - 잘못된 토큰 형식") + void catchMiLogout_invalid_token_format() throws Exception { + // given + String invalidToken = "InvalidToken"; + + willThrow(new CustomException(ErrorCode.INVALID_AUTH_TOKEN)) + .given(logoutRedisService).addTokenToBlacklist(invalidToken); + + + // when & then + mockMvc.perform(post("/api/members/logout") + .header(HttpHeaders.AUTHORIZATION, invalidToken)) + .andExpect(status().isBadRequest()); + } + } } \ No newline at end of file diff --git a/src/test/java/com/example/mate/domain/member/integration/MemberIntegrationTest.java b/src/test/java/com/example/mate/domain/member/integration/MemberIntegrationTest.java index 42f6081d..2d356f30 100644 --- a/src/test/java/com/example/mate/domain/member/integration/MemberIntegrationTest.java +++ b/src/test/java/com/example/mate/domain/member/integration/MemberIntegrationTest.java @@ -25,6 +25,7 @@ import com.example.mate.domain.member.entity.Member; import com.example.mate.domain.member.repository.FollowRepository; import com.example.mate.domain.member.repository.MemberRepository; +import com.example.mate.domain.member.service.LogoutRedisService; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.EntityManager; import org.junit.jupiter.api.BeforeEach; @@ -35,6 +36,9 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.jdbc.core.JdbcTemplate; @@ -44,9 +48,13 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.concurrent.TimeUnit; import static com.example.mate.domain.match.entity.MatchStatus.SCHEDULED; import static com.example.mate.domain.mate.entity.Status.CLOSED; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -96,6 +104,15 @@ class MemberIntegrationTest { @Autowired private EntityManager entityManager; + @Autowired + private LogoutRedisService logoutRedisService; + + @MockBean + private RedisTemplate<String, String> redisTemplate; + + @MockBean + private ValueOperations<String, String> valueOperations; + @MockBean private JwtUtil jwtUtil; @@ -477,4 +494,41 @@ void login_member_fail_non_exists_email() throws Exception { .andExpect(jsonPath("$.message").value("해당 이메일의 회원 정보를 찾을 수 없습니다.")); } } + + @Nested + @DisplayName("회원 로그아웃") + class LogoutMember { + + @Test + @DisplayName("회원 로그아웃 성공") + @WithAuthMember(userId = "customUser", memberId = 1L) + void logout_member_success_with_my_info_denied() throws Exception { + // given + String token = "Bearer accessToken"; + + // when & then + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + doNothing().when(valueOperations).set( + eq("blacklist:" + token.substring(7)), + eq("blacklisted"), + eq(1L), + eq(TimeUnit.MINUTES)); + + mockMvc.perform(post("/api/members/logout") + .header(HttpHeaders.AUTHORIZATION, token)) + .andExpect(status().isNoContent()); + } + + @Test + @DisplayName("회원 로그아웃 실패 - 잘못된 토큰 형식") + void catchMiLogout_invalid_token_format() throws Exception { + // given + String invalidToken = "InvalidToken"; + + // when & then + mockMvc.perform(post("/api/members/logout") + .header(HttpHeaders.AUTHORIZATION, invalidToken)) + .andExpect(status().isBadRequest()); + } + } } \ No newline at end of file diff --git a/src/test/java/com/example/mate/domain/member/service/LogoutRedisServiceTest.java b/src/test/java/com/example/mate/domain/member/service/LogoutRedisServiceTest.java new file mode 100644 index 00000000..31096c4b --- /dev/null +++ b/src/test/java/com/example/mate/domain/member/service/LogoutRedisServiceTest.java @@ -0,0 +1,101 @@ +package com.example.mate.domain.member.service; + +import com.example.mate.common.error.CustomException; +import com.example.mate.common.error.ErrorCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class LogoutRedisServiceTest { + + @InjectMocks + private LogoutRedisService logoutRedisService; + + @Mock + private RedisTemplate<String, String> redisTemplate; + + @Mock + private ValueOperations<String, String> valueOperations; + + @Nested + @DisplayName("회원 로그아웃") + class LogoutMember { + + @Test + @DisplayName("회원 로그아웃 성공 - 블랙리스트에 토큰 추가") + void add_token_to_blacklist_success() { + // given + String token = "Bearer accessToken"; + when(redisTemplate.opsForValue()).thenReturn(valueOperations); + doNothing().when(valueOperations).set( + "blacklist:" + token.substring(7), "blacklisted", 1, TimeUnit.MINUTES + ); + + // when & then + assertDoesNotThrow(() -> logoutRedisService.addTokenToBlacklist(token)); + verify(redisTemplate.opsForValue(), times(1)).set( + "blacklist:accessToken", "blacklisted", 1, TimeUnit.MINUTES + ); + } + + @Test + @DisplayName("회원 로그아웃 실패 - 잘못된 토큰으로 블랙리스트 추가 시 CustomException") + void add_token_to_blacklist_fail_invalid_token() { + // given + String invalidToken = "InvalidToken"; + + // when & then + assertThatThrownBy(() -> logoutRedisService.addTokenToBlacklist(invalidToken)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_AUTH_TOKEN.getMessage()); + } + } + + @Nested + @DisplayName("블랙리스트에 토큰 여부 확인") + class CheckBlacklist { + + @Test + @DisplayName("블랙리스트에 토큰 여부 확인 성공") + void is_token_blacklisted_success() { + // given + String accessToken = "accessToken"; + when(redisTemplate.hasKey("blacklist:" + accessToken)).thenReturn(true); + + // when + boolean result = logoutRedisService.isTokenBlacklisted(accessToken); + + // then + verify(redisTemplate, times(1)).hasKey("blacklist:accessToken"); + assert result; + } + + @Test + @DisplayName("블랙리스트에 토큰 여부 확인 실패 - 블랙리스트에 존재하지 않는 토큰 확인") + void is_token_blacklisted_fail_not_exists_token() { + // given + String accessToken = "accessToken"; + when(redisTemplate.hasKey("blacklist:" + accessToken)).thenReturn(false); + + // when + boolean result = logoutRedisService.isTokenBlacklisted(accessToken); + + // then + verify(redisTemplate, times(1)).hasKey("blacklist:accessToken"); + assert !result; + } + } +}