diff --git a/backend/build.gradle b/backend/build.gradle index 3850c2781..3fa3ef4b5 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -16,6 +16,7 @@ repositories { } dependencies { + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/backend/src/main/java/com/funeat/auth/application/AuthService.java b/backend/src/main/java/com/funeat/auth/application/AuthService.java index 0918aa601..a762f09e9 100644 --- a/backend/src/main/java/com/funeat/auth/application/AuthService.java +++ b/backend/src/main/java/com/funeat/auth/application/AuthService.java @@ -21,8 +21,7 @@ public AuthService(final MemberService memberService, final PlatformUserProvider public SignUserDto loginWithKakao(final String code) { final UserInfoDto userInfoDto = platformUserProvider.getPlatformUser(code); - final SignUserDto signUserDto = memberService.findOrCreateMember(userInfoDto); - return signUserDto; + return memberService.findOrCreateMember(userInfoDto); } public String getLoginRedirectUri() { diff --git a/backend/src/main/java/com/funeat/auth/exception/AuthErrorCode.java b/backend/src/main/java/com/funeat/auth/exception/AuthErrorCode.java new file mode 100644 index 000000000..3508ff142 --- /dev/null +++ b/backend/src/main/java/com/funeat/auth/exception/AuthErrorCode.java @@ -0,0 +1,31 @@ +package com.funeat.auth.exception; + +import org.springframework.http.HttpStatus; + +public enum AuthErrorCode { + + LOGIN_MEMBER_NOT_FOUND(HttpStatus.UNAUTHORIZED, "로그인 하지 않은 회원입니다. 로그인을 해주세요.", "6001"), + ; + + private final HttpStatus status; + private final String message; + private final String code; + + AuthErrorCode(final HttpStatus status, final String message, final String code) { + this.status = status; + this.message = message; + this.code = code; + } + + public HttpStatus getStatus() { + return status; + } + + public String getMessage() { + return message; + } + + public String getCode() { + return code; + } +} diff --git a/backend/src/main/java/com/funeat/auth/exception/AuthException.java b/backend/src/main/java/com/funeat/auth/exception/AuthException.java new file mode 100644 index 000000000..c8ee3ef92 --- /dev/null +++ b/backend/src/main/java/com/funeat/auth/exception/AuthException.java @@ -0,0 +1,18 @@ +package com.funeat.auth.exception; + +import com.funeat.exception.ErrorCode; +import com.funeat.exception.GlobalException; +import org.springframework.http.HttpStatus; + +public class AuthException extends GlobalException { + + public AuthException(final HttpStatus status, final ErrorCode errorCode) { + super(status, errorCode); + } + + public static class NotLoggedInException extends AuthException { + public NotLoggedInException(final AuthErrorCode errorCode) { + super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); + } + } +} diff --git a/backend/src/main/java/com/funeat/auth/exception/LoginException.java b/backend/src/main/java/com/funeat/auth/exception/LoginException.java deleted file mode 100644 index 2b3eaca08..000000000 --- a/backend/src/main/java/com/funeat/auth/exception/LoginException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.funeat.auth.exception; - -public class LoginException extends RuntimeException { - - public LoginException(final String message) { - super(message); - } -} diff --git a/backend/src/main/java/com/funeat/auth/util/AuthHandlerInterceptor.java b/backend/src/main/java/com/funeat/auth/util/AuthHandlerInterceptor.java index f32f791ea..3a74baf8f 100644 --- a/backend/src/main/java/com/funeat/auth/util/AuthHandlerInterceptor.java +++ b/backend/src/main/java/com/funeat/auth/util/AuthHandlerInterceptor.java @@ -1,6 +1,7 @@ package com.funeat.auth.util; -import com.funeat.auth.exception.LoginException; +import com.funeat.auth.exception.AuthErrorCode; +import com.funeat.auth.exception.AuthException.NotLoggedInException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @@ -14,7 +15,7 @@ public class AuthHandlerInterceptor implements HandlerInterceptor { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { final HttpSession session = request.getSession(); if (session.getAttribute("member") == null) { - throw new LoginException("login error"); + throw new NotLoggedInException(AuthErrorCode.LOGIN_MEMBER_NOT_FOUND); } return true; } diff --git a/backend/src/main/java/com/funeat/common/StringToCategoryTypeConverter.java b/backend/src/main/java/com/funeat/common/StringToCategoryTypeConverter.java index 642c82a8c..8873f0a7b 100644 --- a/backend/src/main/java/com/funeat/common/StringToCategoryTypeConverter.java +++ b/backend/src/main/java/com/funeat/common/StringToCategoryTypeConverter.java @@ -7,6 +7,6 @@ public class StringToCategoryTypeConverter implements Converter { + + private final String code; + private final String message; + private T info; + + public ErrorCode(final String code, final String message, final T info) { + this.code = code; + this.message = message; + this.info = info; + } + + public ErrorCode(final String code, final String message) { + this.code = code; + this.message = message; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } + + public T getInfo() { + return info; + } +} diff --git a/backend/src/main/java/com/funeat/exception/GlobalException.java b/backend/src/main/java/com/funeat/exception/GlobalException.java new file mode 100644 index 000000000..976619a36 --- /dev/null +++ b/backend/src/main/java/com/funeat/exception/GlobalException.java @@ -0,0 +1,23 @@ +package com.funeat.exception; + +import org.springframework.http.HttpStatus; + +public class GlobalException extends RuntimeException { + + private final HttpStatus status; + private final ErrorCode errorCode; + + public GlobalException(final HttpStatus status, final ErrorCode errorCode) { + super(errorCode.getMessage()); + this.status = status; + this.errorCode = errorCode; + } + + public HttpStatus getStatus() { + return status; + } + + public ErrorCode getErrorCode() { + return errorCode; + } +} diff --git a/backend/src/main/java/com/funeat/exception/presentation/GlobalControllerAdvice.java b/backend/src/main/java/com/funeat/exception/presentation/GlobalControllerAdvice.java index 0ff01c73c..3d806575d 100644 --- a/backend/src/main/java/com/funeat/exception/presentation/GlobalControllerAdvice.java +++ b/backend/src/main/java/com/funeat/exception/presentation/GlobalControllerAdvice.java @@ -1,25 +1,102 @@ package com.funeat.exception.presentation; -import com.funeat.auth.exception.LoginException; +import static com.funeat.exception.CommonErrorCode.REQUEST_VALID_ERROR_CODE; +import static com.funeat.exception.CommonErrorCode.UNKNOWN_SERVER_ERROR_CODE; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.funeat.exception.ErrorCode; +import com.funeat.exception.GlobalException; +import java.util.stream.Collectors; import javax.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; @RestControllerAdvice public class GlobalControllerAdvice { private final Logger log = LoggerFactory.getLogger(this.getClass()); + private final ObjectMapper objectMapper; + + public GlobalControllerAdvice(final ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @ExceptionHandler({MethodArgumentTypeMismatchException.class, MissingServletRequestParameterException.class}) + public ResponseEntity handleParamValidationException(final Exception e, final HttpServletRequest request) { + log.warn("{} = {}, code = {} message = {}", request.getMethod(), request.getRequestURI(), + REQUEST_VALID_ERROR_CODE.getCode(), e.getMessage()); + + final ErrorCode errorCode = new ErrorCode<>(REQUEST_VALID_ERROR_CODE.getCode(), + REQUEST_VALID_ERROR_CODE.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorCode); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleParamValidationException(final MethodArgumentNotValidException e, + final HttpServletRequest request) { + final String filedErrorLogMessages = getMethodArgumentExceptionLogMessage(e); + + final String errorMessage = e.getBindingResult() + .getAllErrors() + .stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining(", ")); + + final String responseErrorMessage = errorMessage + ". " + REQUEST_VALID_ERROR_CODE.getMessage(); + + final ErrorCode errorCode = new ErrorCode<>(REQUEST_VALID_ERROR_CODE.getCode(), responseErrorMessage); - @ExceptionHandler(LoginException.class) - public ResponseEntity loginExceptionHandler(final LoginException e, final HttpServletRequest request) { + log.warn("{} = {}, message = {} ", request.getMethod(), request.getRequestURI(), + filedErrorLogMessages); + return ResponseEntity.status(REQUEST_VALID_ERROR_CODE.getStatus()).body(errorCode); + } + + private static String getMethodArgumentExceptionLogMessage(final MethodArgumentNotValidException e) { + final String filedErrorMessages = e.getBindingResult() + .getFieldErrors() + .stream() + .map(FieldError::getField) + .collect(Collectors.joining(", ")); + + return filedErrorMessages + " 요청 실패"; + } + + @ExceptionHandler(GlobalException.class) + public ResponseEntity handleGlobalException(final GlobalException e, final HttpServletRequest request) + throws JsonProcessingException { + final String exceptionSource = getExceptionSource(e); + log.warn("source = {} , {} = {} code = {} message = {} info = {}", exceptionSource, request.getMethod(), + request.getRequestURI(), e.getErrorCode().getCode(), e.getErrorCode().getMessage(), + objectMapper.writeValueAsString(e.getErrorCode().getInfo())); + + final ErrorCode errorCode = new ErrorCode<>(e.getErrorCode().getCode(), e.getMessage()); + return ResponseEntity.status(e.getStatus()).body(errorCode); + } + + private String getExceptionSource(final Exception e) { + final StackTraceElement[] stackTrace = e.getStackTrace(); + if (stackTrace.length > 0) { + return stackTrace[0].toString(); + } + return "Unknown location"; + } - log.warn("URI: {}, 쿠키값: {}, 저장된 JSESSIONID 값: {}", request.getRequestURI(), request.getHeader("Cookie"), - request.getSession().getId()); + @ExceptionHandler(Exception.class) + public ResponseEntity handleServerException(final Exception e) { + log.error("", e); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage()); + final ErrorCode errorCode = new ErrorCode<>(UNKNOWN_SERVER_ERROR_CODE.getCode(), + UNKNOWN_SERVER_ERROR_CODE.getMessage()); + return ResponseEntity.status(UNKNOWN_SERVER_ERROR_CODE.getStatus()).body(errorCode); } } diff --git a/backend/src/main/java/com/funeat/member/application/MemberService.java b/backend/src/main/java/com/funeat/member/application/MemberService.java index 8d34a2236..506d8cdc2 100644 --- a/backend/src/main/java/com/funeat/member/application/MemberService.java +++ b/backend/src/main/java/com/funeat/member/application/MemberService.java @@ -7,6 +7,8 @@ import com.funeat.member.domain.Member; import com.funeat.member.dto.MemberProfileResponse; import com.funeat.member.dto.MemberRequest; +import com.funeat.member.exception.MemberErrorCode; +import com.funeat.member.exception.MemberException.MemberNotFoundException; import com.funeat.member.persistence.MemberRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -39,7 +41,7 @@ private SignUserDto save(final UserInfoDto userInfoDto) { public MemberProfileResponse getMemberProfile(final Long memberId) { final Member findMember = memberRepository.findById(memberId) - .orElseThrow(IllegalArgumentException::new); + .orElseThrow(() -> new MemberNotFoundException(MemberErrorCode.MEMBER_NOT_FOUND, memberId)); return MemberProfileResponse.toResponse(findMember); } @@ -47,7 +49,7 @@ public MemberProfileResponse getMemberProfile(final Long memberId) { @Transactional public void modify(final Long memberId, final MemberRequest request) { final Member findMember = memberRepository.findById(memberId) - .orElseThrow(IllegalArgumentException::new); + .orElseThrow(() -> new MemberNotFoundException(MemberErrorCode.MEMBER_NOT_FOUND, memberId)); final String nickname = request.getNickname(); final String profileImage = request.getProfileImage(); diff --git a/backend/src/main/java/com/funeat/member/dto/MemberRequest.java b/backend/src/main/java/com/funeat/member/dto/MemberRequest.java index a4800c6ed..557ad201c 100644 --- a/backend/src/main/java/com/funeat/member/dto/MemberRequest.java +++ b/backend/src/main/java/com/funeat/member/dto/MemberRequest.java @@ -1,8 +1,13 @@ package com.funeat.member.dto; +import javax.validation.constraints.NotBlank; + public class MemberRequest { + @NotBlank(message = "닉네임을 확인해주세요") private final String nickname; + + @NotBlank(message = "프로필 이미지를 확인해주세요") private final String profileImage; public MemberRequest(final String nickname, final String profileImage) { diff --git a/backend/src/main/java/com/funeat/member/exception/MemberErrorCode.java b/backend/src/main/java/com/funeat/member/exception/MemberErrorCode.java new file mode 100644 index 000000000..7a4d30651 --- /dev/null +++ b/backend/src/main/java/com/funeat/member/exception/MemberErrorCode.java @@ -0,0 +1,31 @@ +package com.funeat.member.exception; + +import org.springframework.http.HttpStatus; + +public enum MemberErrorCode { + + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다. 회원 id를 확인하세요.", "5001"), + ; + + private final HttpStatus status; + private final String message; + private final String code; + + MemberErrorCode(final HttpStatus status, final String message, final String code) { + this.status = status; + this.message = message; + this.code = code; + } + + public HttpStatus getStatus() { + return status; + } + + public String getMessage() { + return message; + } + + public String getCode() { + return code; + } +} diff --git a/backend/src/main/java/com/funeat/member/exception/MemberException.java b/backend/src/main/java/com/funeat/member/exception/MemberException.java new file mode 100644 index 000000000..92d24ce02 --- /dev/null +++ b/backend/src/main/java/com/funeat/member/exception/MemberException.java @@ -0,0 +1,18 @@ +package com.funeat.member.exception; + +import com.funeat.exception.ErrorCode; +import com.funeat.exception.GlobalException; +import org.springframework.http.HttpStatus; + +public class MemberException extends GlobalException { + + public MemberException(final HttpStatus status, final ErrorCode errorCode) { + super(status, errorCode); + } + + public static class MemberNotFoundException extends MemberException { + public MemberNotFoundException(final MemberErrorCode errorCode, final Long memberId) { + super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), memberId)); + } + } +} diff --git a/backend/src/main/java/com/funeat/member/presentation/MemberApiController.java b/backend/src/main/java/com/funeat/member/presentation/MemberApiController.java index 546c5665d..a01ea5b2a 100644 --- a/backend/src/main/java/com/funeat/member/presentation/MemberApiController.java +++ b/backend/src/main/java/com/funeat/member/presentation/MemberApiController.java @@ -5,6 +5,7 @@ import com.funeat.member.application.MemberService; import com.funeat.member.dto.MemberProfileResponse; import com.funeat.member.dto.MemberRequest; +import javax.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -32,7 +33,7 @@ public ResponseEntity getMemberProfile( @PutMapping("/api/members") public ResponseEntity putMemberProfile(@AuthenticationPrincipal final LoginInfo loginInfo, - @RequestBody final MemberRequest request) { + @RequestBody @Valid final MemberRequest request) { final Long memberId = loginInfo.getId(); memberService.modify(memberId, request); diff --git a/backend/src/main/java/com/funeat/product/application/ProductService.java b/backend/src/main/java/com/funeat/product/application/ProductService.java index f4c864df7..0e5287bbd 100644 --- a/backend/src/main/java/com/funeat/product/application/ProductService.java +++ b/backend/src/main/java/com/funeat/product/application/ProductService.java @@ -1,5 +1,8 @@ package com.funeat.product.application; +import static com.funeat.product.exception.CategoryErrorCode.CATEGORY_NOT_FOUND; +import static com.funeat.product.exception.ProductErrorCode.PRODUCT_NOT_FOUND; + import com.funeat.product.domain.Category; import com.funeat.product.domain.Product; import com.funeat.product.dto.ProductInCategoryDto; @@ -9,6 +12,8 @@ import com.funeat.product.dto.ProductsInCategoryResponse; import com.funeat.product.dto.RankingProductDto; import com.funeat.product.dto.RankingProductsResponse; +import com.funeat.product.exception.CategoryException.CategoryNotFoundException; +import com.funeat.product.exception.ProductException.ProductNotFoundException; import com.funeat.product.persistence.CategoryRepository; import com.funeat.product.persistence.ProductRepository; import com.funeat.review.persistence.ReviewTagRepository; @@ -44,7 +49,7 @@ public ProductService(final CategoryRepository categoryRepository, final Product public ProductsInCategoryResponse getAllProductsInCategory(final Long categoryId, final Pageable pageable) { final Category category = categoryRepository.findById(categoryId) - .orElseThrow(IllegalArgumentException::new); + .orElseThrow(() -> new CategoryNotFoundException(CATEGORY_NOT_FOUND, categoryId)); final Page pages = getAllProductsInCategory(pageable, category); @@ -64,7 +69,7 @@ private Page getAllProductsInCategory(final Pageable pagea public ProductResponse findProductDetail(final Long productId) { final Product product = productRepository.findById(productId) - .orElseThrow(IllegalArgumentException::new); + .orElseThrow(() -> new ProductNotFoundException(PRODUCT_NOT_FOUND, productId)); final List tags = reviewTagRepository.findTop3TagsByReviewIn(productId, PageRequest.of(TOP, THREE)); diff --git a/backend/src/main/java/com/funeat/product/domain/CategoryType.java b/backend/src/main/java/com/funeat/product/domain/CategoryType.java index ee55c80b6..6568dea89 100644 --- a/backend/src/main/java/com/funeat/product/domain/CategoryType.java +++ b/backend/src/main/java/com/funeat/product/domain/CategoryType.java @@ -1,5 +1,17 @@ package com.funeat.product.domain; +import static com.funeat.product.exception.CategoryErrorCode.CATEGORY_TYPE_NOT_FOUND; + +import com.funeat.product.exception.CategoryException.CategoryTypeNotFoundException; +import java.util.Arrays; + public enum CategoryType { - FOOD, STORE + FOOD, STORE; + + public static CategoryType findCategoryType(final String type) { + return Arrays.stream(values()) + .filter(it -> it.name().equals(type.toUpperCase())) + .findFirst() + .orElseThrow(() -> new CategoryTypeNotFoundException(CATEGORY_TYPE_NOT_FOUND, type)); + } } diff --git a/backend/src/main/java/com/funeat/product/exception/CategoryErrorCode.java b/backend/src/main/java/com/funeat/product/exception/CategoryErrorCode.java new file mode 100644 index 000000000..5214a03b6 --- /dev/null +++ b/backend/src/main/java/com/funeat/product/exception/CategoryErrorCode.java @@ -0,0 +1,32 @@ +package com.funeat.product.exception; + +import org.springframework.http.HttpStatus; + +public enum CategoryErrorCode { + + CATEGORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 카테고리입니다. 카테고리 id를 확인하세요.", "2001"), + CATEGORY_TYPE_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 카테고리 타입입니다. 카테고리 타입을 확인하세요.", "2002"), + ; + + private final HttpStatus status; + private final String message; + private final String code; + + CategoryErrorCode(final HttpStatus status, final String message, final String code) { + this.status = status; + this.message = message; + this.code = code; + } + + public HttpStatus getStatus() { + return status; + } + + public String getMessage() { + return message; + } + + public String getCode() { + return code; + } +} diff --git a/backend/src/main/java/com/funeat/product/exception/CategoryException.java b/backend/src/main/java/com/funeat/product/exception/CategoryException.java new file mode 100644 index 000000000..6402c250a --- /dev/null +++ b/backend/src/main/java/com/funeat/product/exception/CategoryException.java @@ -0,0 +1,24 @@ +package com.funeat.product.exception; + +import com.funeat.exception.ErrorCode; +import com.funeat.exception.GlobalException; +import org.springframework.http.HttpStatus; + +public class CategoryException extends GlobalException { + + public CategoryException(final HttpStatus status, final ErrorCode errorCode) { + super(status, errorCode); + } + + public static class CategoryNotFoundException extends CategoryException { + public CategoryNotFoundException(final CategoryErrorCode errorCode, final Long categoryId) { + super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), categoryId)); + } + } + + public static class CategoryTypeNotFoundException extends CategoryException { + public CategoryTypeNotFoundException(final CategoryErrorCode errorCode, final String type) { + super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), type)); + } + } +} diff --git a/backend/src/main/java/com/funeat/product/exception/ProductErrorCode.java b/backend/src/main/java/com/funeat/product/exception/ProductErrorCode.java new file mode 100644 index 000000000..933f91098 --- /dev/null +++ b/backend/src/main/java/com/funeat/product/exception/ProductErrorCode.java @@ -0,0 +1,31 @@ +package com.funeat.product.exception; + +import org.springframework.http.HttpStatus; + +public enum ProductErrorCode { + + PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 상품입니다. 상품 id를 확인하세요.", "1001"), + ; + + private final HttpStatus status; + private final String message; + private final String code; + + ProductErrorCode(final HttpStatus status, final String message, final String code) { + this.status = status; + this.message = message; + this.code = code; + } + + public HttpStatus getStatus() { + return status; + } + + public String getMessage() { + return message; + } + + public String getCode() { + return code; + } +} diff --git a/backend/src/main/java/com/funeat/product/exception/ProductException.java b/backend/src/main/java/com/funeat/product/exception/ProductException.java new file mode 100644 index 000000000..c9b1f1720 --- /dev/null +++ b/backend/src/main/java/com/funeat/product/exception/ProductException.java @@ -0,0 +1,18 @@ +package com.funeat.product.exception; + +import com.funeat.exception.ErrorCode; +import com.funeat.exception.GlobalException; +import org.springframework.http.HttpStatus; + +public class ProductException extends GlobalException { + + public ProductException(final HttpStatus status, final ErrorCode errorCode) { + super(status, errorCode); + } + + public static class ProductNotFoundException extends ProductException { + public ProductNotFoundException(final ProductErrorCode errorCode, final Long productId) { + super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), productId)); + } + } +} diff --git a/backend/src/main/java/com/funeat/product/presentation/ProductApiController.java b/backend/src/main/java/com/funeat/product/presentation/ProductApiController.java index eaa73e3ff..1b1796204 100644 --- a/backend/src/main/java/com/funeat/product/presentation/ProductApiController.java +++ b/backend/src/main/java/com/funeat/product/presentation/ProductApiController.java @@ -23,10 +23,8 @@ public ProductApiController(final ProductService productService) { } @GetMapping("/categories/{categoryId}/products") - public ResponseEntity getAllProductsInCategory( - @PathVariable final Long categoryId, - @PageableDefault Pageable pageable - ) { + public ResponseEntity getAllProductsInCategory(@PathVariable final Long categoryId, + @PageableDefault Pageable pageable) { final ProductsInCategoryResponse response = productService.getAllProductsInCategory(categoryId, pageable); return ResponseEntity.ok(response); } diff --git a/backend/src/main/java/com/funeat/recipe/application/RecipeService.java b/backend/src/main/java/com/funeat/recipe/application/RecipeService.java index 2a2f3ba4f..ee9563bf1 100644 --- a/backend/src/main/java/com/funeat/recipe/application/RecipeService.java +++ b/backend/src/main/java/com/funeat/recipe/application/RecipeService.java @@ -1,17 +1,24 @@ package com.funeat.recipe.application; +import static com.funeat.member.exception.MemberErrorCode.MEMBER_NOT_FOUND; +import static com.funeat.product.exception.ProductErrorCode.PRODUCT_NOT_FOUND; +import static com.funeat.recipe.exception.RecipeErrorCode.RECIPE_NOT_FOUND; + import com.funeat.common.ImageService; import com.funeat.member.domain.Member; +import com.funeat.member.exception.MemberException.MemberNotFoundException; import com.funeat.member.persistence.MemberRepository; import com.funeat.member.persistence.RecipeFavoriteRepository; import com.funeat.product.domain.Product; import com.funeat.product.domain.ProductRecipe; +import com.funeat.product.exception.ProductException.ProductNotFoundException; import com.funeat.product.persistence.ProductRecipeRepository; import com.funeat.product.persistence.ProductRepository; import com.funeat.recipe.domain.Recipe; import com.funeat.recipe.domain.RecipeImage; import com.funeat.recipe.dto.RecipeCreateRequest; import com.funeat.recipe.dto.RecipeDetailResponse; +import com.funeat.recipe.exception.RecipeException.RecipeNotFoundException; import com.funeat.recipe.persistence.RecipeImageRepository; import com.funeat.recipe.persistence.RecipeRepository; import java.util.List; @@ -49,13 +56,13 @@ public RecipeService(final MemberRepository memberRepository, final ProductRepos @Transactional public Long create(final Long memberId, final List images, final RecipeCreateRequest request) { final Member member = memberRepository.findById(memberId) - .orElseThrow(IllegalArgumentException::new); + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND, memberId)); final Recipe savedRecipe = recipeRepository.save(new Recipe(request.getTitle(), request.getContent(), member)); request.getProductIds() .stream() .map(it -> productRepository.findById(it) - .orElseThrow(IllegalArgumentException::new)) + .orElseThrow(() -> new ProductNotFoundException(PRODUCT_NOT_FOUND, it))) .forEach(it -> productRecipeRepository.save(new ProductRecipe(it, savedRecipe))); if (Objects.nonNull(images)) { @@ -69,7 +76,7 @@ public Long create(final Long memberId, final List images, final public RecipeDetailResponse getRecipeDetail(final Long memberId, final Long recipeId) { final Recipe recipe = recipeRepository.findById(recipeId) - .orElseThrow(IllegalArgumentException::new); + .orElseThrow(() -> new RecipeNotFoundException(RECIPE_NOT_FOUND, recipeId)); final List images = recipeImageRepository.findByRecipe(recipe); final List products = productRecipeRepository.findProductByRecipe(recipe); final Long totalPrice = products.stream() @@ -83,7 +90,7 @@ public RecipeDetailResponse getRecipeDetail(final Long memberId, final Long reci private Boolean calculateFavoriteChecked(final Long memberId, final Recipe recipe) { final Member member = memberRepository.findById(memberId) - .orElseThrow(IllegalArgumentException::new); + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND, memberId)); return recipeFavoriteRepository.existsByMemberAndRecipeAndFavoriteTrue(member, recipe); } } diff --git a/backend/src/main/java/com/funeat/recipe/dto/RecipeCreateRequest.java b/backend/src/main/java/com/funeat/recipe/dto/RecipeCreateRequest.java index fd24536da..201c28cf0 100644 --- a/backend/src/main/java/com/funeat/recipe/dto/RecipeCreateRequest.java +++ b/backend/src/main/java/com/funeat/recipe/dto/RecipeCreateRequest.java @@ -1,11 +1,21 @@ package com.funeat.recipe.dto; import java.util.List; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; public class RecipeCreateRequest { + @NotBlank(message = "꿀조합 이름을 확인해 주세요") private final String title; + + @NotNull(message = "상품 ID 목록을 확인해 주세요") + @Size(min = 1, message = "적어도 1개의 상품 ID가 필요합니다") private final List productIds; + + @NotBlank(message = "꿀조합 내용을 확인해 주세요") + @Size(max = 500, message = "꿀조합 내용은 최대 500자까지 입력 가능합니다") private final String content; public RecipeCreateRequest(final String title, final List productIds, final String content) { diff --git a/backend/src/main/java/com/funeat/recipe/exception/RecipeErrorCode.java b/backend/src/main/java/com/funeat/recipe/exception/RecipeErrorCode.java new file mode 100644 index 000000000..887da8864 --- /dev/null +++ b/backend/src/main/java/com/funeat/recipe/exception/RecipeErrorCode.java @@ -0,0 +1,31 @@ +package com.funeat.recipe.exception; + +import org.springframework.http.HttpStatus; + +public enum RecipeErrorCode { + + RECIPE_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 꿀조합입니다. 꿀조합 id를 확인하세요.", "7001"), + ; + + private final HttpStatus status; + private final String message; + private final String code; + + RecipeErrorCode(final HttpStatus status, final String message, final String code) { + this.status = status; + this.message = message; + this.code = code; + } + + public HttpStatus getStatus() { + return status; + } + + public String getMessage() { + return message; + } + + public String getCode() { + return code; + } +} diff --git a/backend/src/main/java/com/funeat/recipe/exception/RecipeException.java b/backend/src/main/java/com/funeat/recipe/exception/RecipeException.java new file mode 100644 index 000000000..f7352c746 --- /dev/null +++ b/backend/src/main/java/com/funeat/recipe/exception/RecipeException.java @@ -0,0 +1,18 @@ +package com.funeat.recipe.exception; + +import com.funeat.exception.ErrorCode; +import com.funeat.exception.GlobalException; +import org.springframework.http.HttpStatus; + +public class RecipeException extends GlobalException { + + public RecipeException(final HttpStatus status, final ErrorCode errorCode) { + super(status, errorCode); + } + + public static class RecipeNotFoundException extends RecipeException { + public RecipeNotFoundException(final RecipeErrorCode errorCode, final Long RecipeId) { + super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), RecipeId)); + } + } +} diff --git a/backend/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java b/backend/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java index f55cd75f2..36d30e394 100644 --- a/backend/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java +++ b/backend/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java @@ -7,6 +7,7 @@ import com.funeat.recipe.dto.RecipeDetailResponse; import java.net.URI; import java.util.List; +import javax.validation.Valid; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -17,7 +18,7 @@ import org.springframework.web.multipart.MultipartFile; @RestController -public class RecipeApiController implements RecipeController{ +public class RecipeApiController implements RecipeController { private final RecipeService recipeService; @@ -25,10 +26,11 @@ public RecipeApiController(final RecipeService recipeService) { this.recipeService = recipeService; } - @PostMapping(value = "/api/recipes", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, MediaType.APPLICATION_JSON_VALUE}) + @PostMapping(value = "/api/recipes", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE, + MediaType.APPLICATION_JSON_VALUE}) public ResponseEntity writeRecipe(@AuthenticationPrincipal final LoginInfo loginInfo, @RequestPart(required = false) final List images, - @RequestPart final RecipeCreateRequest recipeRequest) { + @RequestPart @Valid final RecipeCreateRequest recipeRequest) { final Long recipeId = recipeService.create(loginInfo.getId(), images, recipeRequest); return ResponseEntity.created(URI.create("/api/recipes/" + recipeId)).build(); diff --git a/backend/src/main/java/com/funeat/recipe/utill/RecipeHandlerInterceptor.java b/backend/src/main/java/com/funeat/recipe/util/RecipeHandlerInterceptor.java similarity index 74% rename from backend/src/main/java/com/funeat/recipe/utill/RecipeHandlerInterceptor.java rename to backend/src/main/java/com/funeat/recipe/util/RecipeHandlerInterceptor.java index 03f2f712f..8bd3b7bcb 100644 --- a/backend/src/main/java/com/funeat/recipe/utill/RecipeHandlerInterceptor.java +++ b/backend/src/main/java/com/funeat/recipe/util/RecipeHandlerInterceptor.java @@ -1,6 +1,7 @@ -package com.funeat.recipe.utill; +package com.funeat.recipe.util; -import com.funeat.auth.exception.LoginException; +import com.funeat.auth.exception.AuthErrorCode; +import com.funeat.auth.exception.AuthException.NotLoggedInException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @@ -17,7 +18,7 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons } final HttpSession session = request.getSession(); if (session.getAttribute("member") == null) { - throw new LoginException("login error"); + throw new NotLoggedInException(AuthErrorCode.LOGIN_MEMBER_NOT_FOUND); } return true; } diff --git a/backend/src/main/java/com/funeat/review/application/ReviewService.java b/backend/src/main/java/com/funeat/review/application/ReviewService.java index 6b9500c28..6a186135c 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewService.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewService.java @@ -1,14 +1,21 @@ package com.funeat.review.application; +import static com.funeat.member.exception.MemberErrorCode.MEMBER_NOT_FOUND; +import static com.funeat.product.exception.ProductErrorCode.PRODUCT_NOT_FOUND; +import static com.funeat.review.exception.ReviewErrorCode.REVIEW_NOT_FOUND; + import com.funeat.common.ImageService; import com.funeat.member.domain.Member; import com.funeat.member.domain.favorite.ReviewFavorite; +import com.funeat.member.exception.MemberException.MemberNotFoundException; import com.funeat.member.persistence.MemberRepository; import com.funeat.member.persistence.ReviewFavoriteRepository; import com.funeat.product.domain.Product; +import com.funeat.product.exception.ProductException.ProductNotFoundException; import com.funeat.product.persistence.ProductRepository; import com.funeat.review.domain.Review; import com.funeat.review.domain.ReviewTag; +import com.funeat.review.exception.ReviewException.ReviewNotFoundException; import com.funeat.review.persistence.ReviewRepository; import com.funeat.review.persistence.ReviewTagRepository; import com.funeat.review.presentation.dto.RankingReviewDto; @@ -58,9 +65,9 @@ public ReviewService(final ReviewRepository reviewRepository, final TagRepositor public void create(final Long productId, final Long memberId, final MultipartFile image, final ReviewCreateRequest reviewRequest) { final Member findMember = memberRepository.findById(memberId) - .orElseThrow(IllegalArgumentException::new); + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND, memberId)); final Product findProduct = productRepository.findById(productId) - .orElseThrow(IllegalArgumentException::new); + .orElseThrow(() -> new ProductNotFoundException(PRODUCT_NOT_FOUND, productId)); final Review savedReview; if (Objects.isNull(image)) { @@ -89,9 +96,9 @@ public void create(final Long productId, final Long memberId, final MultipartFil @Transactional public void likeReview(final Long reviewId, final Long memberId, final ReviewFavoriteRequest request) { final Member findMember = memberRepository.findById(memberId) - .orElseThrow(IllegalArgumentException::new); + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND, memberId)); final Review findReview = reviewRepository.findById(reviewId) - .orElseThrow(IllegalArgumentException::new); + .orElseThrow(() -> new ReviewNotFoundException(REVIEW_NOT_FOUND, reviewId)); final ReviewFavorite savedReviewFavorite = reviewFavoriteRepository.findByMemberAndReview(findMember, findReview).orElseGet(() -> saveReviewFavorite(findMember, findReview, request.getFavorite())); @@ -108,10 +115,10 @@ private ReviewFavorite saveReviewFavorite(final Member member, final Review revi public SortingReviewsResponse sortingReviews(final Long productId, final Pageable pageable, final Long memberId) { final Member member = memberRepository.findById(memberId) - .orElseThrow(IllegalArgumentException::new); + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND, memberId)); final Product product = productRepository.findById(productId) - .orElseThrow(IllegalArgumentException::new); + .orElseThrow(() -> new ProductNotFoundException(PRODUCT_NOT_FOUND, productId)); final Page reviewPage = reviewRepository.findReviewsByProduct(pageable, product); diff --git a/backend/src/main/java/com/funeat/review/exception/ReviewErrorCode.java b/backend/src/main/java/com/funeat/review/exception/ReviewErrorCode.java new file mode 100644 index 000000000..d91c0c8c3 --- /dev/null +++ b/backend/src/main/java/com/funeat/review/exception/ReviewErrorCode.java @@ -0,0 +1,31 @@ +package com.funeat.review.exception; + +import org.springframework.http.HttpStatus; + +public enum ReviewErrorCode { + + REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 리뷰입니다. 리뷰 id를 확인하세요.", "3001"), + ; + + private final HttpStatus status; + private final String message; + private final String code; + + ReviewErrorCode(final HttpStatus status, final String message, final String code) { + this.status = status; + this.message = message; + this.code = code; + } + + public HttpStatus getStatus() { + return status; + } + + public String getMessage() { + return message; + } + + public String getCode() { + return code; + } +} diff --git a/backend/src/main/java/com/funeat/review/exception/ReviewException.java b/backend/src/main/java/com/funeat/review/exception/ReviewException.java new file mode 100644 index 000000000..4699f3af6 --- /dev/null +++ b/backend/src/main/java/com/funeat/review/exception/ReviewException.java @@ -0,0 +1,18 @@ +package com.funeat.review.exception; + +import com.funeat.exception.ErrorCode; +import com.funeat.exception.GlobalException; +import org.springframework.http.HttpStatus; + +public class ReviewException extends GlobalException { + + public ReviewException(final HttpStatus status, final ErrorCode errorCode) { + super(status, errorCode); + } + + public static class ReviewNotFoundException extends ReviewException { + public ReviewNotFoundException(final ReviewErrorCode errorCode, final Long reviewId) { + super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), reviewId)); + } + } +} diff --git a/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java b/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java index d50526079..b7ec97062 100644 --- a/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java +++ b/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java @@ -8,6 +8,7 @@ import com.funeat.review.presentation.dto.ReviewFavoriteRequest; import com.funeat.review.presentation.dto.SortingReviewsResponse; import java.net.URI; +import javax.validation.Valid; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.MediaType; @@ -35,7 +36,7 @@ public ReviewApiController(final ReviewService reviewService) { public ResponseEntity writeReview(@PathVariable final Long productId, @AuthenticationPrincipal final LoginInfo loginInfo, @RequestPart(required = false) final MultipartFile image, - @RequestPart final ReviewCreateRequest reviewRequest) { + @RequestPart @Valid final ReviewCreateRequest reviewRequest) { reviewService.create(productId, loginInfo.getId(), image, reviewRequest); return ResponseEntity.created(URI.create("/api/products/" + productId)).build(); @@ -44,7 +45,7 @@ public ResponseEntity writeReview(@PathVariable final Long productId, @PatchMapping("/api/products/{productId}/reviews/{reviewId}") public ResponseEntity toggleLikeReview(@PathVariable Long reviewId, @AuthenticationPrincipal LoginInfo loginInfo, - @RequestBody ReviewFavoriteRequest request) { + @RequestBody @Valid ReviewFavoriteRequest request) { reviewService.likeReview(reviewId, loginInfo.getId(), request); return ResponseEntity.noContent().build(); diff --git a/backend/src/main/java/com/funeat/review/presentation/dto/ReviewCreateRequest.java b/backend/src/main/java/com/funeat/review/presentation/dto/ReviewCreateRequest.java index 127bd347d..aeda36555 100644 --- a/backend/src/main/java/com/funeat/review/presentation/dto/ReviewCreateRequest.java +++ b/backend/src/main/java/com/funeat/review/presentation/dto/ReviewCreateRequest.java @@ -1,12 +1,24 @@ package com.funeat.review.presentation.dto; import java.util.List; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; public class ReviewCreateRequest { + @NotNull(message = "평점을 확인해 주세요") private final Long rating; + + @NotNull(message = "태그 ID 목록을 확인해 주세요") + @Size(min = 1, message = "적어도 1개의 태그 ID가 필요합니다") private final List tagIds; + + @NotBlank(message = "리뷰 내용을 확인해 주세요") + @Size(max = 200, message = "리뷰 내용은 최대 200자까지 입력 가능합니다") private final String content; + + @NotNull(message = "재구매 여부를 입력해주세요") private final Boolean rebuy; public ReviewCreateRequest(final Long rating, final List tagIds, final String content, final Boolean rebuy) { diff --git a/backend/src/main/java/com/funeat/review/presentation/dto/ReviewFavoriteRequest.java b/backend/src/main/java/com/funeat/review/presentation/dto/ReviewFavoriteRequest.java index 6dce84076..973462d64 100644 --- a/backend/src/main/java/com/funeat/review/presentation/dto/ReviewFavoriteRequest.java +++ b/backend/src/main/java/com/funeat/review/presentation/dto/ReviewFavoriteRequest.java @@ -2,9 +2,11 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import javax.validation.constraints.NotNull; public class ReviewFavoriteRequest { + @NotNull(message = "좋아요를 확인해주세요") private final Boolean favorite; @JsonCreator diff --git a/backend/src/test/java/com/funeat/acceptance/common/CommonSteps.java b/backend/src/test/java/com/funeat/acceptance/common/CommonSteps.java index e1617999e..dc8d201b8 100644 --- a/backend/src/test/java/com/funeat/acceptance/common/CommonSteps.java +++ b/backend/src/test/java/com/funeat/acceptance/common/CommonSteps.java @@ -14,6 +14,9 @@ public class CommonSteps { public static final HttpStatus 정상_생성 = HttpStatus.CREATED; public static final HttpStatus 정상_처리_NO_CONTENT = HttpStatus.NO_CONTENT; public static final HttpStatus 리다이렉션_영구_이동 = HttpStatus.FOUND; + public static final HttpStatus 인증되지_않음 = HttpStatus.UNAUTHORIZED; + public static final HttpStatus 잘못된_요청 = HttpStatus.BAD_REQUEST; + public static final HttpStatus 찾을수_없음 = HttpStatus.NOT_FOUND; public static Long LOCATION_헤더에서_ID_추출(final ExtractableResponse response) { return Long.parseLong(response.header(LOCATION).split("/")[2]); diff --git a/backend/src/test/java/com/funeat/acceptance/member/MemberAcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/member/MemberAcceptanceTest.java index 642a6c4fa..afe78f147 100644 --- a/backend/src/test/java/com/funeat/acceptance/member/MemberAcceptanceTest.java +++ b/backend/src/test/java/com/funeat/acceptance/member/MemberAcceptanceTest.java @@ -2,9 +2,13 @@ import static com.funeat.acceptance.auth.LoginSteps.로그인_쿠키를_얻는다; import static com.funeat.acceptance.common.CommonSteps.STATUS_CODE를_검증한다; +import static com.funeat.acceptance.common.CommonSteps.인증되지_않음; +import static com.funeat.acceptance.common.CommonSteps.잘못된_요청; import static com.funeat.acceptance.common.CommonSteps.정상_처리; import static com.funeat.acceptance.member.MemberSteps.사용자_정보_수정_요청; import static com.funeat.acceptance.member.MemberSteps.사용자_정보_조회_요청; +import static com.funeat.auth.exception.AuthErrorCode.LOGIN_MEMBER_NOT_FOUND; +import static com.funeat.exception.CommonErrorCode.REQUEST_VALID_ERROR_CODE; import static com.funeat.fixture.MemberFixture.멤버_멤버1_생성; import static org.assertj.core.api.SoftAssertions.assertSoftly; @@ -16,6 +20,8 @@ import io.restassured.response.Response; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; @SuppressWarnings("NonAsciiCharacters") public class MemberAcceptanceTest extends AcceptanceTest { @@ -40,6 +46,20 @@ class getMemberProfile_성공_테스트 { } } + @Nested + class getMemberProfile_실패_테스트 { + + @Test + void 로그인_하지않은_사용자가_사용자_정보를_확인시_예외가_발생한다() { + // given & when + final var response = 사용자_정보_조회_요청(null); + + // then + STATUS_CODE를_검증한다(response, 인증되지_않음); + 사용자_승인되지_않음을_검증하다(response); + } + } + @Nested class putMemberProfile_성공_테스트 { @@ -60,6 +80,75 @@ class putMemberProfile_성공_테스트 { } } + @Nested + class putMemberProfile_실패_테스트 { + + @Test + void 로그인_하지않은_사용자가_사용자_정보_수정시_예외가_발생한다() { + // given + final var request = new MemberRequest("after", "http://www.after.com"); + + // when + final var response = 사용자_정보_수정_요청(null, request); + + // then + STATUS_CODE를_검증한다(response, 인증되지_않음); + 사용자_승인되지_않음을_검증하다(response); + } + + @ParameterizedTest + @NullAndEmptySource + void 사용자가_사용자_정보_수정할때_닉네임_미기입시_예외가_발생한다(final String nickname) { + // given + final var member = 멤버_멤버1_생성(); + 단일_멤버_저장(member); + + final var loginCookie = 로그인_쿠키를_얻는다(); + final var request = new MemberRequest(nickname, "http://www.after.com"); + + // when + final var response = 사용자_정보_수정_요청(loginCookie, request); + + // then + final var expectedCode = REQUEST_VALID_ERROR_CODE.getCode(); + final var expectedMessage = "닉네임을 확인해주세요. " + REQUEST_VALID_ERROR_CODE.getMessage(); + + STATUS_CODE를_검증한다(response, 잘못된_요청); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + + @ParameterizedTest + @NullAndEmptySource + void 사용자가_사용자_정보_수정할때_이미지_미기입시_예외가_발생한다(final String image) { + // given + final var member = 멤버_멤버1_생성(); + 단일_멤버_저장(member); + + final var loginCookie = 로그인_쿠키를_얻는다(); + final var request = new MemberRequest("test", image); + + // when + final var response = 사용자_정보_수정_요청(loginCookie, request); + + // then + final var expectedCode = REQUEST_VALID_ERROR_CODE.getCode(); + final var expectedMessage = "프로필 이미지를 확인해주세요. " + REQUEST_VALID_ERROR_CODE.getMessage(); + + STATUS_CODE를_검증한다(response, 잘못된_요청); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + } + + private void RESPONSE_CODE와_MESSAGE를_검증한다(final ExtractableResponse response, final String expectedCode, + final String expectedMessage) { + assertSoftly(softAssertions -> { + softAssertions.assertThat(response.jsonPath().getString("code")) + .isEqualTo(expectedCode); + softAssertions.assertThat(response.jsonPath().getString("message")) + .isEqualTo(expectedMessage); + }); + } + private void 사용자_정보_조회를_검증하다(final ExtractableResponse response, final Member member) { final var expected = MemberProfileResponse.toResponse(member); final var expectedNickname = expected.getNickname(); @@ -75,4 +164,19 @@ class putMemberProfile_성공_테스트 { .isEqualTo(expectedProfileImage); }); } + + private void 사용자_승인되지_않음을_검증하다(final ExtractableResponse response) { + final var expectedCode = LOGIN_MEMBER_NOT_FOUND.getCode(); + final var expectedMessage = LOGIN_MEMBER_NOT_FOUND.getMessage(); + + final var actualCode = response.jsonPath().getString("code"); + final var actualMessage = response.jsonPath().getString("message"); + + assertSoftly(softAssertions -> { + softAssertions.assertThat(actualCode) + .isEqualTo(expectedCode); + softAssertions.assertThat(actualMessage) + .isEqualTo(expectedMessage); + }); + } } diff --git a/backend/src/test/java/com/funeat/acceptance/product/CategoryAcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/product/CategoryAcceptanceTest.java index 56e5b6170..3c95eae80 100644 --- a/backend/src/test/java/com/funeat/acceptance/product/CategoryAcceptanceTest.java +++ b/backend/src/test/java/com/funeat/acceptance/product/CategoryAcceptanceTest.java @@ -1,8 +1,10 @@ package com.funeat.acceptance.product; import static com.funeat.acceptance.common.CommonSteps.STATUS_CODE를_검증한다; +import static com.funeat.acceptance.common.CommonSteps.잘못된_요청; import static com.funeat.acceptance.common.CommonSteps.정상_처리; -import static com.funeat.acceptance.product.CategorySteps.공통_상품_카테고리_목록_조회_요청; +import static com.funeat.acceptance.product.CategorySteps.카테고리_목록_조회_요청; +import static com.funeat.exception.CommonErrorCode.REQUEST_VALID_ERROR_CODE; import static com.funeat.fixture.CategoryFixture.카테고리_CU_생성; import static com.funeat.fixture.CategoryFixture.카테고리_간편식사_생성; import static com.funeat.fixture.CategoryFixture.카테고리_과자류_생성; @@ -18,6 +20,9 @@ import java.util.stream.Collectors; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; @SuppressWarnings("NonAsciiCharacters") public class CategoryAcceptanceTest extends AcceptanceTest { @@ -36,7 +41,7 @@ class getAllCategoriesByType_성공_테스트 { 복수_카테고리_저장(간편식사, 즉석조리, 과자류, CU); // when - final var response = 공통_상품_카테고리_목록_조회_요청(); + final var response = 카테고리_목록_조회_요청("food"); // then STATUS_CODE를_검증한다(response, 정상_처리); @@ -44,6 +49,24 @@ class getAllCategoriesByType_성공_테스트 { } } + @Nested + class getAllCategoriesByType_실패_테스트 { + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"a", "foo"}) + void 존재하지_않는_카테고리의_목록을_조회할때_예외가_발생한다(final String type) { + // given & when + final var response = 카테고리_목록_조회_요청(type); + + // then + final var expectedCode = REQUEST_VALID_ERROR_CODE.getCode(); + + STATUS_CODE를_검증한다(response, 잘못된_요청); + assertThat(response.jsonPath().getString("code")).isEqualTo(expectedCode); + } + } + private void 공통_상품_카테고리_목록_조회_결과를_검증한다(final ExtractableResponse response, final List categories) { final var expected = categories.stream() diff --git a/backend/src/test/java/com/funeat/acceptance/product/CategorySteps.java b/backend/src/test/java/com/funeat/acceptance/product/CategorySteps.java index 9ed5aa8de..342beb2fc 100644 --- a/backend/src/test/java/com/funeat/acceptance/product/CategorySteps.java +++ b/backend/src/test/java/com/funeat/acceptance/product/CategorySteps.java @@ -8,9 +8,9 @@ @SuppressWarnings("NonAsciiCharacters") public class CategorySteps { - public static ExtractableResponse 공통_상품_카테고리_목록_조회_요청() { + public static ExtractableResponse 카테고리_목록_조회_요청(final String type) { return given() - .queryParam("type", "food") + .queryParam("type", type) .when() .get("/api/categories") .then() diff --git a/backend/src/test/java/com/funeat/acceptance/product/ProductAcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/product/ProductAcceptanceTest.java index f46e7e38c..3256a61e9 100644 --- a/backend/src/test/java/com/funeat/acceptance/product/ProductAcceptanceTest.java +++ b/backend/src/test/java/com/funeat/acceptance/product/ProductAcceptanceTest.java @@ -3,6 +3,7 @@ import static com.funeat.acceptance.auth.LoginSteps.로그인_쿠키를_얻는다; import static com.funeat.acceptance.common.CommonSteps.STATUS_CODE를_검증한다; import static com.funeat.acceptance.common.CommonSteps.정상_처리; +import static com.funeat.acceptance.common.CommonSteps.찾을수_없음; import static com.funeat.acceptance.product.ProductSteps.상품_랭킹_조회_요청; import static com.funeat.acceptance.product.ProductSteps.상품_상세_조회_요청; import static com.funeat.acceptance.product.ProductSteps.카테고리별_상품_목록_조회_요청; @@ -38,7 +39,10 @@ import static com.funeat.fixture.TagFixture.태그_간식_ETC_생성; import static com.funeat.fixture.TagFixture.태그_단짠단짠_TASTE_생성; import static com.funeat.fixture.TagFixture.태그_맛있어요_TASTE_생성; +import static com.funeat.product.exception.CategoryErrorCode.CATEGORY_NOT_FOUND; +import static com.funeat.product.exception.ProductErrorCode.PRODUCT_NOT_FOUND; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import com.funeat.acceptance.common.AcceptanceTest; import com.funeat.product.domain.Product; @@ -431,6 +435,33 @@ class 리뷰수_기준_내림차순으로_카테고리별_상품_목록_조회 { } } + @Nested + class getAllProductsInCategory_실패_테스트 { + + @Test + void 상품을_정렬할때_카테고리가_존재하지_않으면_예외가_발생한다() { + // given + final var notExistCategoryId = 99999L; + + // when + final var response = 카테고리별_상품_목록_조회_요청(notExistCategoryId, "price", "desc", 0); + + // then + STATUS_CODE를_검증한다(response, 찾을수_없음); + RESPONSE_CODE와_MESSAGE를_검증한다(response, CATEGORY_NOT_FOUND.getCode(), CATEGORY_NOT_FOUND.getMessage()); + } + } + + private void RESPONSE_CODE와_MESSAGE를_검증한다(final ExtractableResponse response, final String expectedCode, + final String expectedMessage) { + assertSoftly(softAssertions -> { + softAssertions.assertThat(response.jsonPath().getString("code")) + .isEqualTo(expectedCode); + softAssertions.assertThat(response.jsonPath().getString("message")) + .isEqualTo(expectedMessage); + }); + } + @Nested class getProductDetail_성공_테스트 { @@ -471,6 +502,23 @@ class getProductDetail_성공_테스트 { } } + @Nested + class getProductDetail_실패_테스트 { + + @Test + void 존재하지_않는_상품_상세_정보를_조회할때_예외가_발생한다() { + // given + final var notExistProductId = 99999L; + + // when + final var response = 상품_상세_조회_요청(notExistProductId); + + // then + STATUS_CODE를_검증한다(response, 찾을수_없음); + RESPONSE_CODE와_MESSAGE를_검증한다(response, PRODUCT_NOT_FOUND.getCode(), PRODUCT_NOT_FOUND.getMessage()); + } + } + @Nested class getRankingProducts_성공_테스트 { diff --git a/backend/src/test/java/com/funeat/acceptance/recipe/RecipeAcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/recipe/RecipeAcceptanceTest.java index a7a97a9cf..36e94f003 100644 --- a/backend/src/test/java/com/funeat/acceptance/recipe/RecipeAcceptanceTest.java +++ b/backend/src/test/java/com/funeat/acceptance/recipe/RecipeAcceptanceTest.java @@ -2,27 +2,40 @@ import static com.funeat.acceptance.auth.LoginSteps.로그인_쿠키를_얻는다; import static com.funeat.acceptance.common.CommonSteps.STATUS_CODE를_검증한다; +import static com.funeat.acceptance.common.CommonSteps.인증되지_않음; +import static com.funeat.acceptance.common.CommonSteps.잘못된_요청; import static com.funeat.acceptance.common.CommonSteps.정상_생성; import static com.funeat.acceptance.common.CommonSteps.정상_처리; +import static com.funeat.acceptance.common.CommonSteps.찾을수_없음; import static com.funeat.acceptance.recipe.RecipeSteps.레시피_상세_정보_요청; import static com.funeat.acceptance.recipe.RecipeSteps.레시피_생성_요청; import static com.funeat.acceptance.recipe.RecipeSteps.레시피_추가_요청하고_id_반환; import static com.funeat.acceptance.recipe.RecipeSteps.여러_사진_요청; +import static com.funeat.auth.exception.AuthErrorCode.LOGIN_MEMBER_NOT_FOUND; +import static com.funeat.exception.CommonErrorCode.REQUEST_VALID_ERROR_CODE; import static com.funeat.fixture.CategoryFixture.카테고리_간편식사_생성; import static com.funeat.fixture.ProductFixture.상품_삼각김밥_가격1000원_평점1점_생성; import static com.funeat.fixture.ProductFixture.상품_삼각김밥_가격2000원_평점1점_생성; import static com.funeat.fixture.ProductFixture.상품_삼각김밥_가격3000원_평점1점_생성; import static com.funeat.fixture.RecipeFixture.레시피추가요청_생성; +import static com.funeat.recipe.exception.RecipeErrorCode.RECIPE_NOT_FOUND; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import com.funeat.acceptance.common.AcceptanceTest; import com.funeat.product.domain.Product; +import com.funeat.recipe.dto.RecipeCreateRequest; import com.funeat.recipe.dto.RecipeDetailResponse; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; @SuppressWarnings("NonAsciiCharacters") public class RecipeAcceptanceTest extends AcceptanceTest { @@ -56,6 +69,168 @@ class writeRecipe_성공_테스트 { } } + @Nested + class writeRecipe_실패_테스트 { + + @Test + void 로그인_하지않은_사용자가_레시피_작성시_예외가_발생한다() { + // given + final var category = 카테고리_간편식사_생성(); + 단일_카테고리_저장(category); + + final var product1 = 상품_삼각김밥_가격1000원_평점1점_생성(category); + final var product2 = 상품_삼각김밥_가격3000원_평점1점_생성(category); + final var product3 = 상품_삼각김밥_가격2000원_평점1점_생성(category); + 복수_상품_저장(product1, product2, product3); + + final var productIds = 상품_아이디_변환(product1, product2, product3); + final var request = 레시피추가요청_생성(productIds); + + final var images = 여러_사진_요청(3); + + // when + final var response = 레시피_생성_요청(request, images, null); + + // then + final var expectedCode = LOGIN_MEMBER_NOT_FOUND.getCode(); + final var expectedMessage = LOGIN_MEMBER_NOT_FOUND.getMessage(); + + STATUS_CODE를_검증한다(response, 인증되지_않음); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + + @ParameterizedTest + @NullAndEmptySource + void 사용자가_레시피_작성할때_레시피이름_미기입시_예외가_발생한다(final String title) { + // given + final var category = 카테고리_간편식사_생성(); + 단일_카테고리_저장(category); + + final var product1 = 상품_삼각김밥_가격1000원_평점1점_생성(category); + final var product2 = 상품_삼각김밥_가격3000원_평점1점_생성(category); + final var product3 = 상품_삼각김밥_가격2000원_평점1점_생성(category); + 복수_상품_저장(product1, product2, product3); + + final var productIds = 상품_아이디_변환(product1, product2, product3); + final var request = new RecipeCreateRequest(title, productIds, "밥 추가, 밥 추가, 밥 추가.. 끝!!"); + + final var images = 여러_사진_요청(3); + + final var loginCookie = 로그인_쿠키를_얻는다(); + + // when + final var response = 레시피_생성_요청(request, images, loginCookie); + + // then + final var expectedCode = REQUEST_VALID_ERROR_CODE.getCode(); + final var expectedMessage = "꿀조합 이름을 확인해 주세요. " + REQUEST_VALID_ERROR_CODE.getMessage(); + + STATUS_CODE를_검증한다(response, 잘못된_요청); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + + @Test + void 사용자가_레시피_작성할때_상품들이_NULL일시_예외가_발생한다() { + // given + final var request = new RecipeCreateRequest("title", null, "밥 추가, 밥 추가, 밥 추가.. 끝!!"); + + final var images = 여러_사진_요청(3); + + final var loginCookie = 로그인_쿠키를_얻는다(); + + // when + final var response = 레시피_생성_요청(request, images, loginCookie); + + // then + final var expectedCode = REQUEST_VALID_ERROR_CODE.getCode(); + final var expectedMessage = "상품 ID 목록을 확인해 주세요. " + REQUEST_VALID_ERROR_CODE.getMessage(); + + STATUS_CODE를_검증한다(response, 잘못된_요청); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + + @Test + void 사용자가_레시피_작성할때_상품들이_비어있을시_예외가_발생한다() { + // given + final var request = new RecipeCreateRequest("title", Collections.emptyList(), "밥 추가, 밥 추가, 밥 추가.. 끝!!"); + + final var images = 여러_사진_요청(3); + + final var loginCookie = 로그인_쿠키를_얻는다(); + + // when + final var response = 레시피_생성_요청(request, images, loginCookie); + + // then + final var expectedCode = REQUEST_VALID_ERROR_CODE.getCode(); + final var expectedMessage = "적어도 1개의 상품 ID가 필요합니다. " + REQUEST_VALID_ERROR_CODE.getMessage(); + + STATUS_CODE를_검증한다(response, 잘못된_요청); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + + @ParameterizedTest + @NullAndEmptySource + void 사용자가_레시피_작성할때_내용이_비어있을시_예외가_발생한다(final String content) { + // given + final var category = 카테고리_간편식사_생성(); + 단일_카테고리_저장(category); + + final var product1 = 상품_삼각김밥_가격1000원_평점1점_생성(category); + final var product2 = 상품_삼각김밥_가격3000원_평점1점_생성(category); + final var product3 = 상품_삼각김밥_가격2000원_평점1점_생성(category); + 복수_상품_저장(product1, product2, product3); + + final var productIds = 상품_아이디_변환(product1, product2, product3); + + final var request = new RecipeCreateRequest("title", productIds, content); + + final var images = 여러_사진_요청(3); + + final var loginCookie = 로그인_쿠키를_얻는다(); + + // when + final var response = 레시피_생성_요청(request, images, loginCookie); + + // then + final var expectedCode = REQUEST_VALID_ERROR_CODE.getCode(); + final var expectedMessage = "꿀조합 내용을 확인해 주세요. " + REQUEST_VALID_ERROR_CODE.getMessage(); + + STATUS_CODE를_검증한다(response, 잘못된_요청); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + + @Test + void 사용자가_레시피_작성할때_레시피내용이_500자_초과시_예외가_발생한다() { + // given + final var category = 카테고리_간편식사_생성(); + 단일_카테고리_저장(category); + + final var product1 = 상품_삼각김밥_가격1000원_평점1점_생성(category); + final var product2 = 상품_삼각김밥_가격3000원_평점1점_생성(category); + final var product3 = 상품_삼각김밥_가격2000원_평점1점_생성(category); + 복수_상품_저장(product1, product2, product3); + + final var productIds = 상품_아이디_변환(product1, product2, product3); + + final var images = 여러_사진_요청(3); + + final var loginCookie = 로그인_쿠키를_얻는다(); + + // when + final var maxContent = "tests".repeat(100) + "a"; + final var request = new RecipeCreateRequest("title", productIds, maxContent); + final var response = 레시피_생성_요청(request, images, loginCookie); + + // then + final var expectedCode = REQUEST_VALID_ERROR_CODE.getCode(); + final var expectedMessage = "꿀조합 내용은 최대 500자까지 입력 가능합니다. " + REQUEST_VALID_ERROR_CODE.getMessage(); + + STATUS_CODE를_검증한다(response, 잘못된_요청); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + } + @Nested class getRecipeDetail_성공_테스트 { @@ -94,11 +269,39 @@ class getRecipeDetail_성공_테스트 { } } + @Nested + class getRecipeDetail_실패_테스트 { + + @Test + void 존재하지_않는_레시피_사용자가_레시피_상세_조회시_예외가_발생한다() { + // given + final var notExistRecipeId = 99999L; + final var loginCookie = 로그인_쿠키를_얻는다(); + + // when + final var response = 레시피_상세_정보_요청(loginCookie, notExistRecipeId); + + // then + STATUS_CODE를_검증한다(response, 찾을수_없음); + RESPONSE_CODE와_MESSAGE를_검증한다(response, RECIPE_NOT_FOUND.getCode(), RECIPE_NOT_FOUND.getMessage()); + } + } + private void 레시피_상세_정보_조회_결과를_검증한다(final RecipeDetailResponse actual, final RecipeDetailResponse expected) { assertThat(actual).usingRecursiveComparison() .isEqualTo(expected); } + private void RESPONSE_CODE와_MESSAGE를_검증한다(final ExtractableResponse response, final String expectedCode, + final String expectedMessage) { + assertSoftly(softAssertions -> { + softAssertions.assertThat(response.jsonPath().getString("code")) + .isEqualTo(expectedCode); + softAssertions.assertThat(response.jsonPath().getString("message")) + .isEqualTo(expectedMessage); + }); + } + private Long 상품_총가격_계산(final Product... products) { return Stream.of(products) .mapToLong(Product::getPrice) diff --git a/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java index 6f90bf517..f3804e241 100644 --- a/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java +++ b/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java @@ -2,14 +2,19 @@ import static com.funeat.acceptance.auth.LoginSteps.로그인_쿠키를_얻는다; import static com.funeat.acceptance.common.CommonSteps.STATUS_CODE를_검증한다; +import static com.funeat.acceptance.common.CommonSteps.인증되지_않음; +import static com.funeat.acceptance.common.CommonSteps.잘못된_요청; import static com.funeat.acceptance.common.CommonSteps.정상_생성; import static com.funeat.acceptance.common.CommonSteps.정상_처리; import static com.funeat.acceptance.common.CommonSteps.정상_처리_NO_CONTENT; +import static com.funeat.acceptance.common.CommonSteps.찾을수_없음; import static com.funeat.acceptance.review.ReviewSteps.단일_리뷰_요청; import static com.funeat.acceptance.review.ReviewSteps.리뷰_랭킹_조회_요청; import static com.funeat.acceptance.review.ReviewSteps.리뷰_사진_명세_요청; import static com.funeat.acceptance.review.ReviewSteps.리뷰_좋아요_요청; import static com.funeat.acceptance.review.ReviewSteps.정렬된_리뷰_목록_조회_요청; +import static com.funeat.auth.exception.AuthErrorCode.LOGIN_MEMBER_NOT_FOUND; +import static com.funeat.exception.CommonErrorCode.REQUEST_VALID_ERROR_CODE; import static com.funeat.fixture.CategoryFixture.카테고리_즉석조리_생성; import static com.funeat.fixture.MemberFixture.멤버_멤버1_생성; import static com.funeat.fixture.MemberFixture.멤버_멤버2_생성; @@ -27,6 +32,8 @@ import static com.funeat.fixture.ReviewFixture.리뷰추가요청_재구매O_생성; import static com.funeat.fixture.TagFixture.태그_맛있어요_TASTE_생성; import static com.funeat.fixture.TagFixture.태그_푸짐해요_PRICE_생성; +import static com.funeat.product.exception.ProductErrorCode.PRODUCT_NOT_FOUND; +import static com.funeat.review.exception.ReviewErrorCode.REVIEW_NOT_FOUND; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; @@ -36,16 +43,21 @@ import com.funeat.product.domain.Category; import com.funeat.review.domain.Review; import com.funeat.review.presentation.dto.RankingReviewDto; +import com.funeat.review.presentation.dto.ReviewCreateRequest; +import com.funeat.review.presentation.dto.ReviewFavoriteRequest; import com.funeat.review.presentation.dto.SortingReviewDto; import com.funeat.review.presentation.dto.SortingReviewsPageDto; import com.funeat.tag.domain.Tag; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; @SuppressWarnings("NonAsciiCharacters") class ReviewAcceptanceTest extends AcceptanceTest { @@ -80,6 +92,209 @@ class writeReview_성공_테스트 { } } + @Nested + class writeReview_실패_테스트 { + + @Test + void 로그인_하지않은_사용자가_리뷰_작성시_예외가_발생한다() { + // given + final var category = 카테고리_즉석조리_생성(); + 카테고리_단일_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점3점_생성(category); + final var productId = 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + final var tag2 = 태그_푸짐해요_PRICE_생성(); + 복수_태그_저장(tag1, tag2); + + final var tagIds = 태그_아이디_변환(tag1, tag2); + + final var image = 리뷰_사진_명세_요청(); + final var request = 리뷰추가요청_재구매O_생성(4L, tagIds); + + // when + final var response = 단일_리뷰_요청(productId, image, request, null); + + // then + final var expectedCode = LOGIN_MEMBER_NOT_FOUND.getCode(); + final var expectedMessage = LOGIN_MEMBER_NOT_FOUND.getMessage(); + + STATUS_CODE를_검증한다(response, 인증되지_않음); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + + @Test + void 사용자가_리뷰_작성할때_태그들이_NULL일시_예외가_발생한다() { + // given + final var category = 카테고리_즉석조리_생성(); + 카테고리_단일_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점3점_생성(category); + final var productId = 단일_상품_저장(product); + + final var image = 리뷰_사진_명세_요청(); + final var loginCookie = 로그인_쿠키를_얻는다(); + + // when + final var request = 리뷰추가요청_재구매O_생성(4L, null); + final var response = 단일_리뷰_요청(productId, image, request, loginCookie); + + // then + final var expectedCode = REQUEST_VALID_ERROR_CODE.getCode(); + final var expectedMessage = "태그 ID 목록을 확인해 주세요. " + REQUEST_VALID_ERROR_CODE.getMessage(); + + STATUS_CODE를_검증한다(response, 잘못된_요청); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + + @Test + void 사용자가_리뷰_작성할때_태그들이_비어있을시_예외가_발생한다() { + // given + final var category = 카테고리_즉석조리_생성(); + 카테고리_단일_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점3점_생성(category); + final var productId = 단일_상품_저장(product); + + final var image = 리뷰_사진_명세_요청(); + final var loginCookie = 로그인_쿠키를_얻는다(); + + // when + final var request = 리뷰추가요청_재구매O_생성(4L, Collections.emptyList()); + final var response = 단일_리뷰_요청(productId, image, request, loginCookie); + + // then + final var expectedCode = REQUEST_VALID_ERROR_CODE.getCode(); + final var expectedMessage = "적어도 1개의 태그 ID가 필요합니다. " + REQUEST_VALID_ERROR_CODE.getMessage(); + + STATUS_CODE를_검증한다(response, 잘못된_요청); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + + @Test + void 사용자가_리뷰_작성할때_평점이_비어있을시_예외가_발생한다() { + // given + final var category = 카테고리_즉석조리_생성(); + 카테고리_단일_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점3점_생성(category); + final var productId = 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + final var tag2 = 태그_푸짐해요_PRICE_생성(); + 복수_태그_저장(tag1, tag2); + + final var tagIds = 태그_아이디_변환(tag1, tag2); + + final var image = 리뷰_사진_명세_요청(); + final var loginCookie = 로그인_쿠키를_얻는다(); + + // when + final var request = 리뷰추가요청_재구매O_생성(null, tagIds); + final var response = 단일_리뷰_요청(productId, image, request, loginCookie); + + // then + final var expectedCode = REQUEST_VALID_ERROR_CODE.getCode(); + final var expectedMessage = "평점을 확인해 주세요. " + REQUEST_VALID_ERROR_CODE.getMessage(); + + STATUS_CODE를_검증한다(response, 잘못된_요청); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + + @ParameterizedTest + @NullAndEmptySource + void 사용자가_리뷰_작성할때_리뷰내용이_비어있을시_예외가_발생한다(final String content) { + // given + final var category = 카테고리_즉석조리_생성(); + 카테고리_단일_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점3점_생성(category); + final var productId = 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + final var tag2 = 태그_푸짐해요_PRICE_생성(); + 복수_태그_저장(tag1, tag2); + + final var tagIds = 태그_아이디_변환(tag1, tag2); + + final var image = 리뷰_사진_명세_요청(); + final var loginCookie = 로그인_쿠키를_얻는다(); + + // when + final var request = new ReviewCreateRequest(1L, tagIds, content, true); + final var response = 단일_리뷰_요청(productId, image, request, loginCookie); + + // then + final var expectedCode = REQUEST_VALID_ERROR_CODE.getCode(); + final var expectedMessage = "리뷰 내용을 확인해 주세요. " + REQUEST_VALID_ERROR_CODE.getMessage(); + + STATUS_CODE를_검증한다(response, 잘못된_요청); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + + @Test + void 사용자가_리뷰_작성할때_재구매여부가_비어있을시_예외가_발생한다() { + // given + final var category = 카테고리_즉석조리_생성(); + 카테고리_단일_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점3점_생성(category); + final var productId = 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + final var tag2 = 태그_푸짐해요_PRICE_생성(); + 복수_태그_저장(tag1, tag2); + + final var tagIds = 태그_아이디_변환(tag1, tag2); + + final var image = 리뷰_사진_명세_요청(); + final var loginCookie = 로그인_쿠키를_얻는다(); + + // when + final var request = new ReviewCreateRequest(1L, tagIds, "content", null); + final var response = 단일_리뷰_요청(productId, image, request, loginCookie); + + // then + final var expectedCode = REQUEST_VALID_ERROR_CODE.getCode(); + final var expectedMessage = "재구매 여부를 입력해주세요. " + REQUEST_VALID_ERROR_CODE.getMessage(); + + STATUS_CODE를_검증한다(response, 잘못된_요청); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + + @Test + void 사용자가_리뷰_작성할때_리뷰내용이_200자_초과시_예외가_발생한다() { + // given + final var category = 카테고리_즉석조리_생성(); + 카테고리_단일_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점3점_생성(category); + final var productId = 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + final var tag2 = 태그_푸짐해요_PRICE_생성(); + 복수_태그_저장(tag1, tag2); + + final var tagIds = 태그_아이디_변환(tag1, tag2); + + final var image = 리뷰_사진_명세_요청(); + final var loginCookie = 로그인_쿠키를_얻는다(); + + // when + final var maxContent = "test".repeat(50) + "a"; + final var request = new ReviewCreateRequest(1L, tagIds, maxContent, true); + final var response = 단일_리뷰_요청(productId, image, request, loginCookie); + + // then + final var expectedCode = REQUEST_VALID_ERROR_CODE.getCode(); + final var expectedMessage = "리뷰 내용은 최대 200자까지 입력 가능합니다. " + REQUEST_VALID_ERROR_CODE.getMessage(); + + STATUS_CODE를_검증한다(response, 잘못된_요청); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + } + @Nested class toggleLikeReview_성공_테스트 { @@ -159,6 +374,105 @@ class toggleLikeReview_성공_테스트 { } } + @Nested + class toggleLikeReview_실패_테스트 { + + @Test + void 로그인_하지않은_사용자가_리뷰에_좋아요를_할때_예외가_발생한다() { + // given + final var member = 멤버_멤버1_생성(); + final var memberId = 단일_멤버_저장(member); + + final var category = 카테고리_즉석조리_생성(); + 카테고리_단일_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점3점_생성(category); + final var productId = 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + final var tag2 = 태그_푸짐해요_PRICE_생성(); + 복수_태그_저장(tag1, tag2); + + final var tagIds = 태그_아이디_변환(tag1, tag2); + + final var image = 리뷰_사진_명세_요청(); + final var reviewRequest = 리뷰추가요청_재구매O_생성(4L, tagIds); + final var loginCookie = 로그인_쿠키를_얻는다(); + 단일_리뷰_요청(productId, image, reviewRequest, loginCookie); + + final var reviewId = reviewRepository.findAll().get(0).getId(); + final var favoriteRequest = 리뷰좋아요요청_true_생성(); + + // when + final var response = 리뷰_좋아요_요청(productId, reviewId, favoriteRequest, null); + + // then + final var expectedCode = LOGIN_MEMBER_NOT_FOUND.getCode(); + final var expectedMessage = LOGIN_MEMBER_NOT_FOUND.getMessage(); + + STATUS_CODE를_검증한다(response, 인증되지_않음); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + + @Test + void 사용자가_리뷰에_좋아요를_할때_좋아요_미기입시_예외가_발생한다() { + // given + final var member = 멤버_멤버1_생성(); + final var memberId = 단일_멤버_저장(member); + + final var category = 카테고리_즉석조리_생성(); + 카테고리_단일_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점3점_생성(category); + final var productId = 단일_상품_저장(product); + + final var tag1 = 태그_맛있어요_TASTE_생성(); + final var tag2 = 태그_푸짐해요_PRICE_생성(); + 복수_태그_저장(tag1, tag2); + + final var tagIds = 태그_아이디_변환(tag1, tag2); + + final var image = 리뷰_사진_명세_요청(); + final var reviewRequest = 리뷰추가요청_재구매O_생성(4L, tagIds); + final var loginCookie = 로그인_쿠키를_얻는다(); + 단일_리뷰_요청(productId, image, reviewRequest, loginCookie); + + final var reviewId = reviewRepository.findAll().get(0).getId(); + + // when + final var request = new ReviewFavoriteRequest(null); + final var response = 리뷰_좋아요_요청(productId, reviewId, request, loginCookie); + + // then + final var expectedCode = REQUEST_VALID_ERROR_CODE.getCode(); + final var expectedMessage = "좋아요를 확인해주세요. " + REQUEST_VALID_ERROR_CODE.getMessage(); + + STATUS_CODE를_검증한다(response, 잘못된_요청); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + + @Test + void 존재하지_않는_리뷰에_사용자가_좋아요를_할때_예외가_발생한다() { + // given + final var category = 카테고리_즉석조리_생성(); + 카테고리_단일_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점3점_생성(category); + final var productId = 단일_상품_저장(product); + + final var favoriteRequest = 리뷰좋아요요청_true_생성(); + final var loginCookie = 로그인_쿠키를_얻는다(); + + // when + final var notExistReviewId = 99999L; + final var response = 리뷰_좋아요_요청(productId, notExistReviewId, favoriteRequest, loginCookie); + + // then + STATUS_CODE를_검증한다(response, 찾을수_없음); + RESPONSE_CODE와_MESSAGE를_검증한다(response, REVIEW_NOT_FOUND.getCode(), REVIEW_NOT_FOUND.getMessage()); + } + } + @Nested class getSortingReviews_성공_테스트 { @@ -397,6 +711,54 @@ class 최신순으로_리뷰_목록을_조회 { } } + @Nested + class getSortingReviews_실패_테스트 { + + @Test + void 로그인_하지않은_사용자가_리뷰_목록을_조회시_예외가_발생한다() { + // given + final var category = 카테고리_즉석조리_생성(); + 카테고리_단일_저장(category); + + final var product = 상품_삼각김밥_가격1000원_평점3점_생성(category); + final var productId = 단일_상품_저장(product); + + final var member1 = 멤버_멤버1_생성(); + final var member2 = 멤버_멤버2_생성(); + final var member3 = 멤버_멤버3_생성(); + 복수_멤버_저장(member1, member2, member3); + + final var review1 = 리뷰_이미지test3_평점3점_재구매O_생성(member1, product, 5L); + final var review2 = 리뷰_이미지test4_평점4점_재구매O_생성(member2, product, 351L); + final var review3 = 리뷰_이미지test3_평점3점_재구매X_생성(member3, product, 130L); + 복수_리뷰_저장(review1, review2, review3); + + // when + final var response = 정렬된_리뷰_목록_조회_요청(null, productId, "favoriteCount,desc", 0); + + // then + final var expectedCode = LOGIN_MEMBER_NOT_FOUND.getCode(); + final var expectedMessage = LOGIN_MEMBER_NOT_FOUND.getMessage(); + + STATUS_CODE를_검증한다(response, 인증되지_않음); + RESPONSE_CODE와_MESSAGE를_검증한다(response, expectedCode, expectedMessage); + } + + @Test + void 존재하지_않는_상품의_리뷰_목록을_조회시_예외가_발생한다() { + // given + final var notExistProductId = 99999L; + final var loginCookie = 로그인_쿠키를_얻는다(); + + // when + final var response = 정렬된_리뷰_목록_조회_요청(loginCookie, notExistProductId, "favoriteCount,desc", 0); + + // then + STATUS_CODE를_검증한다(response, 찾을수_없음); + RESPONSE_CODE와_MESSAGE를_검증한다(response, PRODUCT_NOT_FOUND.getCode(), PRODUCT_NOT_FOUND.getMessage()); + } + } + @Nested class getRankingReviews_성공_테스트 { @@ -452,6 +814,16 @@ class getRankingReviews_성공_테스트 { }); } + private void RESPONSE_CODE와_MESSAGE를_검증한다(final ExtractableResponse response, final String expectedCode, + final String expectedMessage) { + assertSoftly(softAssertions -> { + softAssertions.assertThat(response.jsonPath().getString("code")) + .isEqualTo(expectedCode); + softAssertions.assertThat(response.jsonPath().getString("message")) + .isEqualTo(expectedMessage); + }); + } + private Long 카테고리_단일_저장(final Category category) { return categoryRepository.save(category).getId(); } diff --git a/backend/src/test/java/com/funeat/member/application/MemberServiceTest.java b/backend/src/test/java/com/funeat/member/application/MemberServiceTest.java index d9c36eb8c..23ca2083f 100644 --- a/backend/src/test/java/com/funeat/member/application/MemberServiceTest.java +++ b/backend/src/test/java/com/funeat/member/application/MemberServiceTest.java @@ -10,6 +10,7 @@ import com.funeat.member.domain.Member; import com.funeat.member.dto.MemberProfileResponse; import com.funeat.member.dto.MemberRequest; +import com.funeat.member.exception.MemberException.MemberNotFoundException; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -109,7 +110,7 @@ class getMemberProfile_실패_테스트 { // when & then assertThatThrownBy(() -> memberService.getMemberProfile(wrongMemberId)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(MemberNotFoundException.class); } } @@ -251,7 +252,7 @@ class modify_실패_테스트 { // when assertThatThrownBy(() -> memberService.modify(wrongMemberId, request)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(MemberNotFoundException.class); } @Test diff --git a/backend/src/test/java/com/funeat/recipe/application/RecipeServiceTest.java b/backend/src/test/java/com/funeat/recipe/application/RecipeServiceTest.java index d84cb9dfe..7df0b4852 100644 --- a/backend/src/test/java/com/funeat/recipe/application/RecipeServiceTest.java +++ b/backend/src/test/java/com/funeat/recipe/application/RecipeServiceTest.java @@ -12,9 +12,11 @@ import com.funeat.common.ServiceTest; import com.funeat.member.domain.Member; +import com.funeat.member.exception.MemberException.MemberNotFoundException; import com.funeat.product.domain.Category; import com.funeat.product.domain.CategoryType; import com.funeat.product.domain.Product; +import com.funeat.product.exception.ProductException.ProductNotFoundException; import com.funeat.recipe.dto.RecipeCreateRequest; import com.funeat.recipe.dto.RecipeDetailResponse; import java.util.List; @@ -129,7 +131,7 @@ class create_실패_테스트 { // when & then assertThatThrownBy(() -> recipeService.create(wrongMemberId, images, request)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(MemberNotFoundException.class); } @Test @@ -157,7 +159,7 @@ class create_실패_테스트 { // when & then assertThatThrownBy(() -> recipeService.create(memberId, images, request)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(ProductNotFoundException.class); } } diff --git a/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java b/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java index 92849b1bf..0f0b49b52 100644 --- a/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java +++ b/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java @@ -26,7 +26,10 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; import com.funeat.common.ServiceTest; +import com.funeat.member.exception.MemberException.MemberNotFoundException; +import com.funeat.product.exception.ProductException.ProductNotFoundException; import com.funeat.review.domain.Review; +import com.funeat.review.exception.ReviewException.ReviewNotFoundException; import com.funeat.review.presentation.dto.SortingReviewDto; import com.funeat.tag.domain.Tag; import java.util.List; @@ -134,7 +137,7 @@ class create_실패_테스트 { // when & then assertThatThrownBy(() -> reviewService.create(productId, wrongMemberId, image, request)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(MemberNotFoundException.class); } @Test @@ -160,7 +163,7 @@ class create_실패_테스트 { // when & then assertThatThrownBy(() -> reviewService.create(wrongProductId, memberId, image, request)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(ProductNotFoundException.class); } } @@ -284,7 +287,7 @@ class likeReview_실패_테스트 { // when assertThatThrownBy(() -> reviewService.likeReview(reviewId, wrongMemberId, favoriteRequest)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(MemberNotFoundException.class); } @Test @@ -316,7 +319,7 @@ class likeReview_실패_테스트 { // when assertThatThrownBy(() -> reviewService.likeReview(wrongReviewId, memberId, favoriteRequest)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(ReviewNotFoundException.class); } } @@ -487,7 +490,7 @@ class sortingReviews_실패_테스트 { // when & then assertThatThrownBy(() -> reviewService.sortingReviews(productId, page, wrongMemberId)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(MemberNotFoundException.class); } @Test @@ -514,7 +517,7 @@ class sortingReviews_실패_테스트 { // when & then assertThatThrownBy(() -> reviewService.sortingReviews(wrongProductId, page, member1Id)) - .isInstanceOf(IllegalArgumentException.class); + .isInstanceOf(ProductNotFoundException.class); } }