diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index 7f9d6a5d..c5678b63 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,5 @@ - - \ No newline at end of file + diff --git a/src/main/java/com/backendoori/ootw/avatar/controller/AvatarItemController.java b/src/main/java/com/backendoori/ootw/avatar/controller/AvatarItemController.java index d66ad894..6050fc13 100644 --- a/src/main/java/com/backendoori/ootw/avatar/controller/AvatarItemController.java +++ b/src/main/java/com/backendoori/ootw/avatar/controller/AvatarItemController.java @@ -3,27 +3,28 @@ import com.backendoori.ootw.avatar.dto.AvatarItemRequest; import com.backendoori.ootw.avatar.dto.AvatarItemResponse; import com.backendoori.ootw.avatar.service.AvatarItemService; +import com.backendoori.ootw.common.validation.Image; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @RestController -@RequestMapping("/api/v1/image") +@RequestMapping("/api/v1/avatar-items") @RequiredArgsConstructor public class AvatarItemController { private final AvatarItemService appearanceService; @PostMapping - public ResponseEntity uploadImage(@RequestPart("file") MultipartFile file, - @RequestBody AvatarItemRequest requestDto) { - AvatarItemResponse avatarItem = appearanceService.uploadItem(file, requestDto); + public ResponseEntity uploadImage(@RequestPart @Image MultipartFile file, + @RequestPart @Valid AvatarItemRequest request) { + AvatarItemResponse avatarItem = appearanceService.uploadItem(file, request); return ResponseEntity.status(HttpStatus.CREATED).body(avatarItem); } diff --git a/src/main/java/com/backendoori/ootw/avatar/domain/AvatarItem.java b/src/main/java/com/backendoori/ootw/avatar/domain/AvatarItem.java index d4bbd125..fcd6b6da 100644 --- a/src/main/java/com/backendoori/ootw/avatar/domain/AvatarItem.java +++ b/src/main/java/com/backendoori/ootw/avatar/domain/AvatarItem.java @@ -29,14 +29,14 @@ public class AvatarItem { @Column(name = "type", nullable = false, columnDefinition = "varchar(30)") @Enumerated(EnumType.STRING) - private Type type; + private ItemType itemType; @Column(name = "sex", nullable = false, columnDefinition = "tinyint") private boolean sex; private AvatarItem(String image, String type, boolean sex) { this.image = image; - this.type = Type.valueOf(type); + this.itemType = ItemType.valueOf(type); this.sex = sex; } diff --git a/src/main/java/com/backendoori/ootw/avatar/domain/Type.java b/src/main/java/com/backendoori/ootw/avatar/domain/ItemType.java similarity index 80% rename from src/main/java/com/backendoori/ootw/avatar/domain/Type.java rename to src/main/java/com/backendoori/ootw/avatar/domain/ItemType.java index 274757f6..ee2d5f60 100644 --- a/src/main/java/com/backendoori/ootw/avatar/domain/Type.java +++ b/src/main/java/com/backendoori/ootw/avatar/domain/ItemType.java @@ -1,5 +1,5 @@ package com.backendoori.ootw.avatar.domain; -public enum Type { +public enum ItemType { HAIR, TOP, PANTS, ACCESSORY, SHOES, BACKGROUND } diff --git a/src/main/java/com/backendoori/ootw/avatar/dto/AvatarItemRequest.java b/src/main/java/com/backendoori/ootw/avatar/dto/AvatarItemRequest.java index 9baed1a7..0a5f2bf2 100644 --- a/src/main/java/com/backendoori/ootw/avatar/dto/AvatarItemRequest.java +++ b/src/main/java/com/backendoori/ootw/avatar/dto/AvatarItemRequest.java @@ -1,7 +1,13 @@ package com.backendoori.ootw.avatar.dto; +import com.backendoori.ootw.avatar.domain.ItemType; +import com.backendoori.ootw.common.validation.Enum; +import jakarta.validation.constraints.NotNull; + public record AvatarItemRequest( + @Enum(enumClass = ItemType.class) String type, + @NotNull boolean sex ) { diff --git a/src/main/java/com/backendoori/ootw/avatar/dto/AvatarItemResponse.java b/src/main/java/com/backendoori/ootw/avatar/dto/AvatarItemResponse.java index 2b32b5ba..b69bc3df 100644 --- a/src/main/java/com/backendoori/ootw/avatar/dto/AvatarItemResponse.java +++ b/src/main/java/com/backendoori/ootw/avatar/dto/AvatarItemResponse.java @@ -9,7 +9,7 @@ public record AvatarItemResponse( ) { public static AvatarItemResponse from(AvatarItem avatarItem) { - return new AvatarItemResponse(avatarItem.getType().name(), + return new AvatarItemResponse(avatarItem.getItemType().name(), avatarItem.isSex(), avatarItem.getImage()); } diff --git a/src/main/java/com/backendoori/ootw/common/AssertUtil.java b/src/main/java/com/backendoori/ootw/common/AssertUtil.java new file mode 100644 index 00000000..e083b4e2 --- /dev/null +++ b/src/main/java/com/backendoori/ootw/common/AssertUtil.java @@ -0,0 +1,33 @@ +package com.backendoori.ootw.common; + +import java.util.Objects; +import java.util.function.Supplier; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class AssertUtil extends Assert { + + public static void notBlank(@Nullable String string, String message) { + if (string == null || string.isBlank()) { + throw new IllegalArgumentException(message); + } + } + + public static void hasPattern(@Nullable String string, @Nullable String pattern, String message) { + notNull(string, message); + + if (!string.matches(Objects.requireNonNull(pattern))) { + throw new IllegalArgumentException(message); + } + } + + public static void throwIf(boolean state, Supplier exceptionSupplier) { + if (state) { + throw exceptionSupplier.get(); + } + } + +} diff --git a/src/main/java/com/backendoori/ootw/common/image/MiniOImageServiceImpl.java b/src/main/java/com/backendoori/ootw/common/image/MiniOImageServiceImpl.java index 7446ce32..0f3bb5e0 100644 --- a/src/main/java/com/backendoori/ootw/common/image/MiniOImageServiceImpl.java +++ b/src/main/java/com/backendoori/ootw/common/image/MiniOImageServiceImpl.java @@ -4,18 +4,19 @@ import java.nio.file.Path; import java.util.concurrent.TimeUnit; import com.backendoori.ootw.config.MiniOConfig; +import com.backendoori.ootw.exception.ImageUploadException; import io.minio.GetPresignedObjectUrlArgs; import io.minio.MinioClient; import io.minio.PutObjectArgs; import io.minio.http.Method; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Controller; +import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @Slf4j -@Controller +@Service @RequiredArgsConstructor public class MiniOImageServiceImpl implements ImageService { @@ -39,7 +40,7 @@ public String uploadImage(MultipartFile file) { .build(); minioClient.putObject(args); } catch (Exception e) { - log.warn("Exception occurred while saving contents : {}", e.getMessage(), e); + throw new ImageUploadException(); } return getUrl(); @@ -56,7 +57,7 @@ private String getUrl() { .expiry(DURATION, TimeUnit.HOURS) .build()); } catch (Exception e) { - log.warn("Exception Occurred while getting: {}", e.getMessage(), e); + throw new ImageUploadException(); } return url; diff --git a/src/main/java/com/backendoori/ootw/common/validation/Enum.java b/src/main/java/com/backendoori/ootw/common/validation/Enum.java new file mode 100644 index 00000000..de8c6e93 --- /dev/null +++ b/src/main/java/com/backendoori/ootw/common/validation/Enum.java @@ -0,0 +1,20 @@ +package com.backendoori.ootw.common.validation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Target(value = {ElementType.PARAMETER, ElementType.FIELD}) +@Retention(value = RetentionPolicy.RUNTIME) +@Constraint(validatedBy = EnumValidator.class) +public @interface Enum { + + String message() default "유효하지 않은 값입니다 다시 입력해주세요"; + Class[] groups() default {}; + Class[] payload() default {}; + Class> enumClass(); + +} diff --git a/src/main/java/com/backendoori/ootw/common/validation/EnumValidator.java b/src/main/java/com/backendoori/ootw/common/validation/EnumValidator.java new file mode 100644 index 00000000..0f3fc64b --- /dev/null +++ b/src/main/java/com/backendoori/ootw/common/validation/EnumValidator.java @@ -0,0 +1,26 @@ +package com.backendoori.ootw.common.validation; + +import java.util.Arrays; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class EnumValidator implements ConstraintValidator { + + private Enum annotation; + + @Override + public void initialize(Enum constraintAnnotation) { + this.annotation = constraintAnnotation; + } + + @Override + public boolean isValid(String type, ConstraintValidatorContext context) { + if (type == null) { + return false; + } + + return Arrays.stream(this.annotation.enumClass().getEnumConstants()) + .anyMatch(e -> e.name().equals(type)); + } + +} diff --git a/src/main/java/com/backendoori/ootw/common/validation/Image.java b/src/main/java/com/backendoori/ootw/common/validation/Image.java new file mode 100644 index 00000000..6376e9ce --- /dev/null +++ b/src/main/java/com/backendoori/ootw/common/validation/Image.java @@ -0,0 +1,23 @@ +package com.backendoori.ootw.common.validation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Target(value = ElementType.PARAMETER) +@Retention(value = RetentionPolicy.RUNTIME) +@Constraint(validatedBy = ImageValidator.class) +public @interface Image { + + String message = "유효하지 않은 이미지를 업로드하였습니다. 다른 이미지를 업로드 해주세요"; + + String message() default message; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/src/main/java/com/backendoori/ootw/common/validation/ImageValidator.java b/src/main/java/com/backendoori/ootw/common/validation/ImageValidator.java new file mode 100644 index 00000000..778c713f --- /dev/null +++ b/src/main/java/com/backendoori/ootw/common/validation/ImageValidator.java @@ -0,0 +1,25 @@ +package com.backendoori.ootw.common.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import org.springframework.web.multipart.MultipartFile; + +public class ImageValidator implements ConstraintValidator { + + @Override + public boolean isValid(MultipartFile img, ConstraintValidatorContext context) { + if(img == null || img.isEmpty()){ + return false; + } + if(img.getSize() > 10_000_000){ + return false; + } + String contentType = img.getContentType(); + if(!contentType.startsWith("image")){ + return false; + } + + return true; + } + +} diff --git a/src/main/java/com/backendoori/ootw/config/MiniOConfig.java b/src/main/java/com/backendoori/ootw/config/MiniOConfig.java index c317d8de..358ce4c9 100644 --- a/src/main/java/com/backendoori/ootw/config/MiniOConfig.java +++ b/src/main/java/com/backendoori/ootw/config/MiniOConfig.java @@ -5,9 +5,9 @@ import io.minio.MinioClient; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; -import org.springframework.stereotype.Component; +import org.springframework.context.annotation.Configuration; -@Component +@Configuration @RequiredArgsConstructor public class MiniOConfig { diff --git a/src/main/java/com/backendoori/ootw/exception/ExceptionResponse.java b/src/main/java/com/backendoori/ootw/exception/ExceptionResponse.java deleted file mode 100644 index 242cfd29..00000000 --- a/src/main/java/com/backendoori/ootw/exception/ExceptionResponse.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.backendoori.ootw.exception; - -import java.util.List; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.MethodArgumentNotValidException; - -public record ExceptionResponse( - T error -) { - - public static ExceptionResponse> from( - MethodArgumentNotValidException e) { - List errors = e.getBindingResult() - .getFieldErrors() - .stream() - .map(fieldError -> - new FieldErrorDetail(fieldError.getField(), fieldError.getDefaultMessage())) - .toList(); - - return new ExceptionResponse<>(errors); - } - - public static ExceptionResponse from(E e) { - return new ExceptionResponse<>(e.getMessage()); - } - - @RequiredArgsConstructor(access = AccessLevel.PROTECTED) - @Getter - public static class FieldErrorDetail { - - private final String field; - private final String defaultMessage; - - } - -} diff --git a/src/main/java/com/backendoori/ootw/exception/GlobalControllerAdvice.java b/src/main/java/com/backendoori/ootw/exception/GlobalControllerAdvice.java index 40cf0a38..cf184ae0 100644 --- a/src/main/java/com/backendoori/ootw/exception/GlobalControllerAdvice.java +++ b/src/main/java/com/backendoori/ootw/exception/GlobalControllerAdvice.java @@ -1,11 +1,16 @@ package com.backendoori.ootw.exception; +import java.util.List; import java.util.NoSuchElementException; +import java.util.Objects; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.dao.DuplicateKeyException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.AuthenticationException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.HandlerMethodValidationException; @@ -14,6 +19,44 @@ @RestControllerAdvice public class GlobalControllerAdvice { + public static final String DEFAULT_MESSAGE = "유효하지 않은 요청 입니다."; + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException e) { + ErrorResponse errorResponse = new ErrorResponse(e.getMessage()); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(errorResponse); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + List errors = e.getFieldErrors(); + String message = errors.stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .filter(Objects::nonNull) + .findFirst() + .orElse(DEFAULT_MESSAGE); + + ErrorResponse errorResponse = new ErrorResponse(message); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(errorResponse); + } + + @ExceptionHandler(HandlerMethodValidationException.class) + public ResponseEntity handlerMethodValidationException(HandlerMethodValidationException e) { + String errorMessage = e.getAllValidationResults() + .get(0) + .getResolvableErrors() + .get(0) + .getDefaultMessage(); + ErrorResponse errorResponse = new ErrorResponse(errorMessage); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(errorResponse); + } + @ExceptionHandler(AuthenticationException.class) public ResponseEntity handleAuthenticationException(AuthenticationException e) { ErrorResponse errorResponse = new ErrorResponse(e.getMessage()); @@ -38,12 +81,11 @@ public ResponseEntity handleDuplicateKeyException(DuplicateKeyExc .body(errorResponse); } - @ExceptionHandler(HandlerMethodValidationException.class) - public ResponseEntity handleHandlerMethodValidationException( - HandlerMethodValidationException e) { - ErrorResponse errorResponse = new ErrorResponse(e.getReason()); + @ExceptionHandler(ImageUploadException.class) + public ResponseEntity handleImageUploadException(ImageUploadException e) { + ErrorResponse errorResponse = new ErrorResponse(e.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST) + return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY) .body(errorResponse); } diff --git a/src/main/java/com/backendoori/ootw/exception/ImageUploadException.java b/src/main/java/com/backendoori/ootw/exception/ImageUploadException.java new file mode 100644 index 00000000..b9646bc0 --- /dev/null +++ b/src/main/java/com/backendoori/ootw/exception/ImageUploadException.java @@ -0,0 +1,12 @@ +package com.backendoori.ootw.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class ImageUploadException extends RuntimeException{ + + private final String message = "이미지 업로드 중 예외가 발생했습니다."; + +} diff --git a/src/main/java/com/backendoori/ootw/post/controller/PostController.java b/src/main/java/com/backendoori/ootw/post/controller/PostController.java index 17e82ea2..5c3ba93a 100644 --- a/src/main/java/com/backendoori/ootw/post/controller/PostController.java +++ b/src/main/java/com/backendoori/ootw/post/controller/PostController.java @@ -2,9 +2,6 @@ import java.net.URI; import java.util.List; -import java.util.NoSuchElementException; -import com.backendoori.ootw.exception.ExceptionResponse; -import com.backendoori.ootw.exception.ExceptionResponse.FieldErrorDetail; import com.backendoori.ootw.post.dto.PostReadResponse; import com.backendoori.ootw.post.dto.PostSaveRequest; import com.backendoori.ootw.post.dto.PostSaveResponse; @@ -13,8 +10,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -55,34 +50,4 @@ public ResponseEntity> readAll() { .body(postService.getAll()); } - @ExceptionHandler(Exception.class) - public ResponseEntity> handleException(Exception e) { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ExceptionResponse.from(e)); - } - - @ExceptionHandler(IllegalArgumentException.class) - public ResponseEntity> handleIllegalArgumentException( - IllegalArgumentException e - ) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ExceptionResponse.from(e)); - } - - @ExceptionHandler(NoSuchElementException.class) - public ResponseEntity> handleNoSuchElementException( - NoSuchElementException e - ) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ExceptionResponse.from(e)); - } - - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity>> handleMethodArgumentNotValidException( - MethodArgumentNotValidException e - ) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST) - .body(ExceptionResponse.from(e)); - } - } diff --git a/src/main/java/com/backendoori/ootw/user/controller/UserController.java b/src/main/java/com/backendoori/ootw/user/controller/UserController.java index fc97f46d..bf9eb119 100644 --- a/src/main/java/com/backendoori/ootw/user/controller/UserController.java +++ b/src/main/java/com/backendoori/ootw/user/controller/UserController.java @@ -6,6 +6,7 @@ import com.backendoori.ootw.user.dto.TokenDto; import com.backendoori.ootw.user.dto.UserDto; import com.backendoori.ootw.user.service.UserService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -23,7 +24,7 @@ public class UserController { private final UserService userService; @PostMapping("/signup") - public ResponseEntity signup(@RequestBody SignupDto signupDto) { + public ResponseEntity signup(@RequestBody @Valid SignupDto signupDto) { UserDto userDto = userService.signup(signupDto); return ResponseEntity.status(HttpStatus.CREATED) @@ -31,7 +32,7 @@ public ResponseEntity signup(@RequestBody SignupDto signupDto) { } @PostMapping("/login") - public ResponseEntity login(@RequestBody LoginDto loginDto) { + public ResponseEntity login(@RequestBody @Valid LoginDto loginDto) { TokenDto tokenDto = userService.login(loginDto); HttpHeaders httpHeaders = new HttpHeaders(); diff --git a/src/main/java/com/backendoori/ootw/user/domain/User.java b/src/main/java/com/backendoori/ootw/user/domain/User.java index 6fdecfff..35f4a99a 100644 --- a/src/main/java/com/backendoori/ootw/user/domain/User.java +++ b/src/main/java/com/backendoori/ootw/user/domain/User.java @@ -1,6 +1,9 @@ package com.backendoori.ootw.user.domain; +import com.backendoori.ootw.common.AssertUtil; import com.backendoori.ootw.common.BaseEntity; +import com.backendoori.ootw.user.validation.Message; +import com.backendoori.ootw.user.validation.RFC5322; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; @@ -8,7 +11,6 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -18,7 +20,6 @@ @Getter @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PROTECTED) public class User extends BaseEntity { @Id @@ -38,4 +39,16 @@ public class User extends BaseEntity { @Column(name = "image") private String image; + public User(Long id, String email, String password, String nickname, String image) { + AssertUtil.hasPattern(email, RFC5322.REGEX, Message.INVALID_EMAIL); + AssertUtil.notBlank(password, Message.BLANK_PASSWORD); + AssertUtil.notBlank(nickname, Message.BLANK_NICKNAME); + + this.id = id; + this.email = email; + this.password = password; + this.nickname = nickname; + this.image = image; + } + } diff --git a/src/main/java/com/backendoori/ootw/user/dto/LoginDto.java b/src/main/java/com/backendoori/ootw/user/dto/LoginDto.java index 1add5ec0..a2667b18 100644 --- a/src/main/java/com/backendoori/ootw/user/dto/LoginDto.java +++ b/src/main/java/com/backendoori/ootw/user/dto/LoginDto.java @@ -1,7 +1,19 @@ package com.backendoori.ootw.user.dto; +import com.backendoori.ootw.user.validation.Password; +import com.backendoori.ootw.user.validation.RFC5322; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + public record LoginDto( + @NotNull + @NotBlank + @Email(regexp = RFC5322.REGEX) String email, + + @NotNull + @Password String password ) { diff --git a/src/main/java/com/backendoori/ootw/user/dto/SignupDto.java b/src/main/java/com/backendoori/ootw/user/dto/SignupDto.java index fea29628..59b368c3 100644 --- a/src/main/java/com/backendoori/ootw/user/dto/SignupDto.java +++ b/src/main/java/com/backendoori/ootw/user/dto/SignupDto.java @@ -1,10 +1,25 @@ package com.backendoori.ootw.user.dto; +import com.backendoori.ootw.user.validation.Message; +import com.backendoori.ootw.user.validation.Password; +import com.backendoori.ootw.user.validation.RFC5322; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + public record SignupDto( + @NotNull + @NotBlank + @Email(regexp = RFC5322.REGEX) String email, + + @NotNull + @Password String password, - String nickname, - String image + + @NotNull + @NotBlank(message = Message.BLANK_NICKNAME) + String nickname ) { } diff --git a/src/main/java/com/backendoori/ootw/exception/AlreadyExistEmailException.java b/src/main/java/com/backendoori/ootw/user/exception/AlreadyExistEmailException.java similarity index 87% rename from src/main/java/com/backendoori/ootw/exception/AlreadyExistEmailException.java rename to src/main/java/com/backendoori/ootw/user/exception/AlreadyExistEmailException.java index 16ae8ab6..d8ec3ae4 100644 --- a/src/main/java/com/backendoori/ootw/exception/AlreadyExistEmailException.java +++ b/src/main/java/com/backendoori/ootw/user/exception/AlreadyExistEmailException.java @@ -1,4 +1,4 @@ -package com.backendoori.ootw.exception; +package com.backendoori.ootw.user.exception; import org.springframework.dao.DuplicateKeyException; diff --git a/src/main/java/com/backendoori/ootw/exception/IncorrectPasswordException.java b/src/main/java/com/backendoori/ootw/user/exception/IncorrectPasswordException.java similarity index 87% rename from src/main/java/com/backendoori/ootw/exception/IncorrectPasswordException.java rename to src/main/java/com/backendoori/ootw/user/exception/IncorrectPasswordException.java index cea57a9b..474f996e 100644 --- a/src/main/java/com/backendoori/ootw/exception/IncorrectPasswordException.java +++ b/src/main/java/com/backendoori/ootw/user/exception/IncorrectPasswordException.java @@ -1,4 +1,4 @@ -package com.backendoori.ootw.exception; +package com.backendoori.ootw.user.exception; import org.springframework.security.core.AuthenticationException; diff --git a/src/main/java/com/backendoori/ootw/user/service/UserService.java b/src/main/java/com/backendoori/ootw/user/service/UserService.java index 72707bc9..082615bc 100644 --- a/src/main/java/com/backendoori/ootw/user/service/UserService.java +++ b/src/main/java/com/backendoori/ootw/user/service/UserService.java @@ -1,7 +1,6 @@ package com.backendoori.ootw.user.service; -import com.backendoori.ootw.exception.AlreadyExistEmailException; -import com.backendoori.ootw.exception.IncorrectPasswordException; +import com.backendoori.ootw.common.AssertUtil; import com.backendoori.ootw.exception.UserNotFoundException; import com.backendoori.ootw.security.jwt.TokenProvider; import com.backendoori.ootw.user.domain.User; @@ -9,11 +8,16 @@ import com.backendoori.ootw.user.dto.SignupDto; import com.backendoori.ootw.user.dto.TokenDto; import com.backendoori.ootw.user.dto.UserDto; +import com.backendoori.ootw.user.exception.AlreadyExistEmailException; +import com.backendoori.ootw.user.exception.IncorrectPasswordException; import com.backendoori.ootw.user.repository.UserRepository; +import com.backendoori.ootw.user.validation.Message; +import com.backendoori.ootw.user.validation.Password; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; @Service @RequiredArgsConstructor @@ -28,16 +32,10 @@ public UserDto signup(SignupDto signupDto) { boolean isAlreadyExistEmail = userRepository.findByEmail(signupDto.email()) .isPresent(); - if (isAlreadyExistEmail) { - throw new AlreadyExistEmailException(); - } + AssertUtil.throwIf(isAlreadyExistEmail, AlreadyExistEmailException::new); + AssertUtil.isTrue(isValidPassword(signupDto.password()), Message.INVALID_PASSWORD); - User user = User.builder() - .email(signupDto.email()) - .password(passwordEncoder.encode(signupDto.password())) - .nickname(signupDto.nickname()) - .image(signupDto.image()) - .build(); + User user = buildUser(signupDto); userRepository.save(user); @@ -47,14 +45,29 @@ public UserDto signup(SignupDto signupDto) { public TokenDto login(LoginDto loginDto) { User user = userRepository.findByEmail(loginDto.email()) .orElseThrow(UserNotFoundException::new); + boolean isIncorrectPassword = !matchPassword(loginDto.password(), user.getPassword()); - if (!passwordEncoder.matches(loginDto.password(), user.getPassword())) { - throw new IncorrectPasswordException(); - } + AssertUtil.throwIf(isIncorrectPassword, IncorrectPasswordException::new); String token = tokenProvider.createToken(user.getId()); return new TokenDto(token); } + private User buildUser(SignupDto signupDto) { + return User.builder() + .email(signupDto.email()) + .password(passwordEncoder.encode(signupDto.password())) + .nickname(signupDto.nickname()) + .build(); + } + + private boolean matchPassword(String decrypted, String encrypted) { + return passwordEncoder.matches(decrypted, encrypted); + } + + private boolean isValidPassword(String password) { + return StringUtils.hasLength(password) && password.matches(Password.REGEX); + } + } diff --git a/src/main/java/com/backendoori/ootw/user/validation/Message.java b/src/main/java/com/backendoori/ootw/user/validation/Message.java new file mode 100644 index 00000000..22e4f08b --- /dev/null +++ b/src/main/java/com/backendoori/ootw/user/validation/Message.java @@ -0,0 +1,18 @@ +package com.backendoori.ootw.user.validation; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class Message { + + public static final String INVALID_EMAIL = "이메일 형식이 올바르지 않습니다."; + + public static final String INVALID_PASSWORD = "비밀번호는 숫자, 영문자, 특수문자를 포함한 " + + Password.MIN_SIZE + "자 이상, " + + Password.MAX_SIZE + "자 이내의 문자여야 합니다."; + + public static final String BLANK_PASSWORD = "비밀번호는 공백일 수 없습니다."; + public static final String BLANK_NICKNAME = "닉네임은 공백일 수 없습니다."; + +} diff --git a/src/main/java/com/backendoori/ootw/user/validation/Password.java b/src/main/java/com/backendoori/ootw/user/validation/Password.java new file mode 100644 index 00000000..764f3095 --- /dev/null +++ b/src/main/java/com/backendoori/ootw/user/validation/Password.java @@ -0,0 +1,28 @@ +package com.backendoori.ootw.user.validation; + +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 jakarta.validation.Constraint; +import jakarta.validation.Payload; + +@Documented +@Constraint(validatedBy = PasswordValidator.class) +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Password { + + int MIN_SIZE = 8; + int MAX_SIZE = 30; + String REGEX = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&^])[A-Za-z\\d@$!%*#?&^]" + + "{" + MIN_SIZE + "," + MAX_SIZE + "}$"; + + String message() default Message.INVALID_PASSWORD; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/src/main/java/com/backendoori/ootw/user/validation/PasswordValidator.java b/src/main/java/com/backendoori/ootw/user/validation/PasswordValidator.java new file mode 100644 index 00000000..2fb653e8 --- /dev/null +++ b/src/main/java/com/backendoori/ootw/user/validation/PasswordValidator.java @@ -0,0 +1,30 @@ +package com.backendoori.ootw.user.validation; + +import java.util.Objects; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class PasswordValidator implements ConstraintValidator { + + @Override + public boolean isValid(String password, ConstraintValidatorContext context) { + if (Objects.isNull(password) || password.isBlank()) { + return violateWithMessage(context, Message.BLANK_PASSWORD); + } + + if (!password.matches(Password.REGEX)) { + return violateWithMessage(context, Message.INVALID_PASSWORD); + } + + return true; + } + + private boolean violateWithMessage(ConstraintValidatorContext context, String message) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(message) + .addConstraintViolation(); + + return false; + } + +} diff --git a/src/main/java/com/backendoori/ootw/user/validation/RFC5322.java b/src/main/java/com/backendoori/ootw/user/validation/RFC5322.java new file mode 100644 index 00000000..1180e8f2 --- /dev/null +++ b/src/main/java/com/backendoori/ootw/user/validation/RFC5322.java @@ -0,0 +1,11 @@ +package com.backendoori.ootw.user.validation; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class RFC5322 { + + public static final String REGEX = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$"; + +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 68b54916..358b9b3a 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -8,6 +8,10 @@ spring: hibernate: ddl-auto: validate open-in-view: false + servlet: + multipart: + max-file-size: 10MB + maxRequestSize: 10MB minio: url: ${MINIO_URL} bucket: ${MINIO_BUCKET} diff --git a/src/test/java/com/backendoori/ootw/avatar/controller/AvatarItemControllerTest.java b/src/test/java/com/backendoori/ootw/avatar/controller/AvatarItemControllerTest.java index d1702f51..f24167e1 100644 --- a/src/test/java/com/backendoori/ootw/avatar/controller/AvatarItemControllerTest.java +++ b/src/test/java/com/backendoori/ootw/avatar/controller/AvatarItemControllerTest.java @@ -3,11 +3,16 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.nio.charset.StandardCharsets; import com.backendoori.ootw.avatar.dto.AvatarItemRequest; import com.backendoori.ootw.avatar.service.AvatarItemService; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.DisplayName; 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.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -17,6 +22,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; +@WithMockUser @AutoConfigureMockMvc @SpringBootTest class AvatarItemControllerTest { @@ -31,23 +37,86 @@ class AvatarItemControllerTest { ObjectMapper objectMapper; @Test - @WithMockUser - @DisplayName("아바타 이미지 업로드 api 테스트") + @DisplayName("아바타 이미지를 정상적으로 등록한다.") public void imageUploadTest() throws Exception { //given MockMultipartFile file = new MockMultipartFile("file", "filename.txt", - "text/plain", "some xml".getBytes()); - AvatarItemRequest request = new AvatarItemRequest("HAIR", true); - String requestJson = objectMapper.writeValueAsString(request); + "image/jpeg", "some xml".getBytes()); + AvatarItemRequest requestDto = new AvatarItemRequest("HAIR", true); + MockMultipartFile request = new MockMultipartFile("request", "filename.txt", + "application/json", objectMapper.writeValueAsBytes(requestDto)); //when, then - mockMvc.perform(multipart("/api/v1/image") + mockMvc.perform(multipart("/api/v1/avatar-items") .file(file) - .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .content(requestJson) - .contentType(MediaType.APPLICATION_JSON) - .characterEncoding("utf-8")) + .file(request) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON) + .characterEncoding(StandardCharsets.UTF_8)) .andExpect(status().isCreated()); } + @ParameterizedTest(name = "[{index}] content-type 이 {0}인 경우") + @ValueSource(strings = {"text/plain", "application/json"}) + @DisplayName("아바타 이미지 업로드 시 파일의 유형이 이미지가 아닌 경우 예외가 발생한다.") + public void imageUpLoadWithInvalidContentType(String contentType) throws Exception { + //given + MockMultipartFile file = new MockMultipartFile("file", "filename.txt", + contentType, "some xml".getBytes()); + AvatarItemRequest dto = new AvatarItemRequest("HAIR", true); + MockMultipartFile requestDto = new MockMultipartFile("request", "filename.json", + MediaType.APPLICATION_JSON_VALUE, objectMapper.writeValueAsBytes(dto)); + + //when, then + mockMvc.perform(multipart("/api/v1/avatar-items") + .file(file) + .file(requestDto) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON) + .characterEncoding(StandardCharsets.UTF_8)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("아바타 이미지 업로드 시 이미지 파일이 없는 경우 예외가 발생한다.") + public void noImageUpLoad() throws Exception { + //given + MockMultipartFile file = new MockMultipartFile("file", "", "image/png", new byte[0]); + AvatarItemRequest dto = new AvatarItemRequest("HAIR", true); + MockMultipartFile requestDto = new MockMultipartFile("request", "filename.json", + MediaType.APPLICATION_JSON_VALUE, objectMapper.writeValueAsBytes(dto)); + + + //when, then + mockMvc.perform(multipart("/api/v1/avatar-items") + .file(file) + .file(requestDto) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON) + .characterEncoding(StandardCharsets.UTF_8)) + .andExpect(status().isBadRequest()); + } + + @ParameterizedTest(name = "[{index}] item type으로 {0}가 들어오는 경우") + @ValueSource(strings = {"afsee", "hair"}) + @NullAndEmptySource + @DisplayName("아바타 이미지 업로드 시 아이템 타입이 존재하지 않는 경우 예외가 발생한다.") + public void UpLoadWithInvalidRequest(String type) throws Exception { + //given + MockMultipartFile file = new MockMultipartFile("file", "filename.txt", + "image/jpeg", "some xml".getBytes()); + AvatarItemRequest requestDto = new AvatarItemRequest(type, true); + MockMultipartFile request = new MockMultipartFile("request", "filename.txt", + "application/json", objectMapper.writeValueAsBytes(requestDto)); + + + //when, then + mockMvc.perform(multipart("/api/v1/avatar-items") + .file(file) + .file(request) + .contentType(MediaType.MULTIPART_FORM_DATA) + .accept(MediaType.APPLICATION_JSON) + .characterEncoding(StandardCharsets.UTF_8)) + .andExpect(status().isBadRequest()); + } } diff --git a/src/test/java/com/backendoori/ootw/avatar/domain/AvatarItemTest.java b/src/test/java/com/backendoori/ootw/avatar/domain/AvatarItemTest.java index 9a262312..b282fe2a 100644 --- a/src/test/java/com/backendoori/ootw/avatar/domain/AvatarItemTest.java +++ b/src/test/java/com/backendoori/ootw/avatar/domain/AvatarItemTest.java @@ -20,7 +20,7 @@ public void createTest() throws Exception { //then - assertThat(request.type()).isEqualTo(avatarItem.getType().name()); + assertThat(request.type()).isEqualTo(avatarItem.getItemType().name()); assertThat(request.sex()).isEqualTo(avatarItem.isSex()); } diff --git a/src/test/java/com/backendoori/ootw/common/image/MiniOImageServiceImplTest.java b/src/test/java/com/backendoori/ootw/common/image/MiniOImageServiceImplTest.java index 7ce4a909..7eba159f 100644 --- a/src/test/java/com/backendoori/ootw/common/image/MiniOImageServiceImplTest.java +++ b/src/test/java/com/backendoori/ootw/common/image/MiniOImageServiceImplTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThatCode; -import com.backendoori.ootw.common.image.ImageService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/test/java/com/backendoori/ootw/post/controller/PostControllerTest.java b/src/test/java/com/backendoori/ootw/post/controller/PostControllerTest.java index 6e3c236a..f82979a1 100644 --- a/src/test/java/com/backendoori/ootw/post/controller/PostControllerTest.java +++ b/src/test/java/com/backendoori/ootw/post/controller/PostControllerTest.java @@ -3,15 +3,15 @@ import static com.backendoori.ootw.security.jwt.JwtAuthenticationFilter.TOKEN_HEADER; import static com.backendoori.ootw.security.jwt.JwtAuthenticationFilter.TOKEN_PREFIX; import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.instanceOf; 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.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.nio.charset.StandardCharsets; import java.util.List; -import com.backendoori.ootw.exception.ExceptionResponse; -import com.backendoori.ootw.exception.ExceptionResponse.FieldErrorDetail; import com.backendoori.ootw.post.dto.PostReadResponse; import com.backendoori.ootw.post.dto.PostSaveRequest; import com.backendoori.ootw.post.dto.PostSaveResponse; @@ -173,16 +173,10 @@ void saveFailByMethodArgumentNotValidException() throws Exception { .accept(MediaType.APPLICATION_JSON); // then - String response = mockMvc.perform(requestBuilder) + mockMvc.perform(requestBuilder) .andExpect(status().isBadRequest()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andReturn() - .getResponse() - .getContentAsString(StandardCharsets.UTF_8); - - ExceptionResponse> exceptionResponse = - objectMapper.readValue(response, ExceptionResponse.class); - assertThat(exceptionResponse.error()).hasSize(1); + .andExpect(jsonPath("$.message", instanceOf(String.class))) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)); } @Test diff --git a/src/test/java/com/backendoori/ootw/post/service/PostServiceTest.java b/src/test/java/com/backendoori/ootw/post/service/PostServiceTest.java index 46471aa3..d81912bc 100644 --- a/src/test/java/com/backendoori/ootw/post/service/PostServiceTest.java +++ b/src/test/java/com/backendoori/ootw/post/service/PostServiceTest.java @@ -109,9 +109,6 @@ void saveFailUserNotFound() { // given setAuthentication(user.getId() + 1); - System.out.println(user.getId() + 1); - System.out.println(userRepository.findAll().stream().map(User::getId).toList()); - WeatherDto weatherDto = new WeatherDto(0.0, -10.0, 10.0, 1, 1); PostSaveRequest postSaveRequest = diff --git a/src/test/java/com/backendoori/ootw/user/controller/UserControllerTest.java b/src/test/java/com/backendoori/ootw/user/controller/UserControllerTest.java index a7e42092..3280063e 100644 --- a/src/test/java/com/backendoori/ootw/user/controller/UserControllerTest.java +++ b/src/test/java/com/backendoori/ootw/user/controller/UserControllerTest.java @@ -10,20 +10,27 @@ import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; -import com.backendoori.ootw.exception.AlreadyExistEmailException; -import com.backendoori.ootw.exception.IncorrectPasswordException; +import java.util.stream.Stream; import com.backendoori.ootw.exception.UserNotFoundException; import com.backendoori.ootw.security.jwt.TokenProvider; import com.backendoori.ootw.user.dto.LoginDto; import com.backendoori.ootw.user.dto.SignupDto; import com.backendoori.ootw.user.dto.TokenDto; import com.backendoori.ootw.user.dto.UserDto; +import com.backendoori.ootw.user.exception.AlreadyExistEmailException; +import com.backendoori.ootw.user.exception.IncorrectPasswordException; import com.backendoori.ootw.user.service.UserService; import com.fasterxml.jackson.databind.ObjectMapper; import net.datafaker.Faker; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; @@ -78,6 +85,68 @@ void created() throws Exception { .andExpect(jsonPath("$.updatedAt", startsWith(removeMills(userDto.updatedAt())))); } + @DisplayName("잘못된 형식의 email일 경우 400 status를 반환한다") + @NullAndEmptySource + @ArgumentsSource(InvalidEmailProvider.class) + @ParameterizedTest + void badRequestInvalidEmail(String email) throws Exception { + // given + String password = faker.internet().password(8, 30, true, true, true); + String nickname = faker.internet().username(); + SignupDto signupDto = new SignupDto(email, password, nickname); + + // when + ResultActions actions = mockMvc.perform( + post("/api/v1/auth/signup") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(signupDto))); + + // then + actions.andExpect(status().isBadRequest()); + } + + @DisplayName("잘못된 형식의 비밀번호의 경우 400 status를 반환한다") + @NullAndEmptySource + @ArgumentsSource(InvalidPasswordProvider.class) + @ParameterizedTest + void badRequestInvalidPassword(String password) throws Exception { + // given + String email = faker.internet().emailAddress(); + String nickname = faker.internet().username(); + SignupDto signupDto = new SignupDto(email, password, nickname); + + // when + ResultActions actions = mockMvc.perform( + post("/api/v1/auth/signup") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(signupDto))); + + // then + actions.andExpect(status().isBadRequest()); + } + + @DisplayName("닉네임이 공백일 경우 400 status를 반환한다") + @NullAndEmptySource + @ParameterizedTest + void badRequestBlankNickname(String nickname) throws Exception { + // given + String email = faker.internet().emailAddress(); + String password = faker.internet().password(8, 30, true, true, true); + SignupDto signupDto = new SignupDto(email, password, nickname); + + // when + ResultActions actions = mockMvc.perform( + post("/api/v1/auth/signup") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(signupDto))); + + // then + actions.andExpect(status().isBadRequest()); + } + @DisplayName("이미 등록된 email일 경우 409 status를 반환한다") @Test void unauthorizedAlreadyExistEmail() throws Exception { @@ -124,6 +193,26 @@ void created() throws Exception { .andExpect(jsonPath("$.token", is(tokenDto.token()))); } + @DisplayName("잘못된 형식의 email일 경우 400 status를 반환한다") + @NullAndEmptySource + @ArgumentsSource(InvalidEmailProvider.class) + @ParameterizedTest + void badRequestInvalidEmail(String email) throws Exception { + // given + String password = faker.internet().password(8, 30, true, true, true); + LoginDto loginDto = new LoginDto(email, password); + + // when + ResultActions actions = mockMvc.perform( + post("/api/v1/auth/login") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginDto))); + + // then + actions.andExpect(status().isBadRequest()); + } + @DisplayName("email이 일치하는 사용자가 없으면 404 status를 반환한다") @Test void unauthorizedNotExistUser() throws Exception { @@ -143,6 +232,26 @@ void unauthorizedNotExistUser() throws Exception { actions.andExpect(status().isNotFound()); } + @DisplayName("잘못된 형식의 비밀번호의 경우 400 status를 반환한다") + @NullAndEmptySource + @ArgumentsSource(InvalidPasswordProvider.class) + @ParameterizedTest + void badRequestInvalidPassword(String password) throws Exception { + // given + String email = faker.internet().emailAddress(); + LoginDto loginDto = new LoginDto(email, password); + + // when + ResultActions actions = mockMvc.perform( + post("/api/v1/auth/login") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(loginDto))); + + // then + actions.andExpect(status().isBadRequest()); + } + @DisplayName("비밀번호가 일치하지 않으면 401 status를 반환한다") @Test void unauthorizedIncorrectPassword() throws Exception { @@ -166,11 +275,10 @@ void unauthorizedIncorrectPassword() throws Exception { private SignupDto generateSignupDto() { String email = faker.internet().emailAddress(); - String password = faker.internet().password(); + String password = faker.internet().password(8, 30, true, true, true); String nickname = faker.internet().username(); - String image = faker.internet().url(); - return new SignupDto(email, password, nickname, image); + return new SignupDto(email, password, nickname); } private UserDto createUser(SignupDto signupDto) { @@ -187,7 +295,7 @@ private UserDto createUser(SignupDto signupDto) { private LoginDto generateLoginDto() { String email = faker.internet().emailAddress(); - String password = faker.internet().password(); + String password = faker.internet().password(8, 30, true, true, true); return new LoginDto(email, password); } @@ -196,4 +304,34 @@ private String removeMills(LocalDateTime localDateTime) { return localDateTime.truncatedTo(ChronoUnit.SECONDS).toString(); } + static class InvalidEmailProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + return Stream.of( + Arguments.of(faker.app().name()), + Arguments.of(faker.name().fullName()), + Arguments.of(faker.internet().url()), + Arguments.of(faker.internet().domainName()), + Arguments.of(faker.internet().webdomain()), + Arguments.of(faker.internet().botUserAgentAny()) + ); + } + + } + + static class InvalidPasswordProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + return Stream.of( + Arguments.of(faker.internet().password(1, 7, true, true, true)), + Arguments.of(faker.internet().password(31, 50, true, true, true)), + Arguments.of(faker.internet().password(8, 30, true, false, true)), + Arguments.of(faker.internet().password(8, 30, true, true, false)) + ); + } + + } + } diff --git a/src/test/java/com/backendoori/ootw/user/domain/UserTest.java b/src/test/java/com/backendoori/ootw/user/domain/UserTest.java new file mode 100644 index 00000000..3f1d58d4 --- /dev/null +++ b/src/test/java/com/backendoori/ootw/user/domain/UserTest.java @@ -0,0 +1,118 @@ +package com.backendoori.ootw.user.domain; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNoException; + +import java.util.stream.Stream; +import com.backendoori.ootw.user.validation.Message; +import net.datafaker.Faker; +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +class UserTest { + + static final Faker faker = new Faker(); + + Long id; + String email; + String password; + String nickname; + String image; + + @BeforeEach + void setup() { + id = (long) faker.number().positive(); + email = faker.internet().emailAddress(); + password = faker.internet().password(); + nickname = faker.internet().username(); + image = faker.internet().url(); + } + + @DisplayName("instance 생성에 성공한다.") + @Test + void testCreate() { + // given + + // when + ThrowingCallable createUser = this::buildUser; + + // then + assertThatNoException().isThrownBy(createUser); + } + + @DisplayName("잘못된 형식의 이메일인 경우 생성에 실패한다.") + @NullAndEmptySource + @MethodSource("generateInvalidEmails") + @ParameterizedTest() + void testCreateInvalidEmail(String email) { + // given + this.email = email; + + // when + ThrowingCallable createUser = this::buildUser; + + // then + assertThatIllegalArgumentException() + .isThrownBy(createUser) + .withMessage(Message.INVALID_EMAIL); + } + + @DisplayName("비밀번호가 공백인 경우 생성에 실패한다.") + @NullAndEmptySource + @ParameterizedTest + void testCreateBlankPassword(String password) { + // given + this.password = password; + + // when + ThrowingCallable createUser = this::buildUser; + + // then + assertThatIllegalArgumentException() + .isThrownBy(createUser) + .withMessage(Message.BLANK_PASSWORD); + } + + @DisplayName("닉네임이 공백인 경우 생성에 실패한다.") + @NullAndEmptySource + @ParameterizedTest + void testCreateBlankNickName(String nickname) { + // given + this.nickname = nickname; + + // when + ThrowingCallable createUser = this::buildUser; + + // then + assertThatIllegalArgumentException() + .isThrownBy(createUser) + .withMessage(Message.BLANK_NICKNAME); + } + + private static Stream generateInvalidEmails() { + return Stream.of( + faker.app().name(), + faker.name().fullName(), + faker.internet().url(), + faker.internet().domainName(), + faker.internet().webdomain(), + faker.internet().botUserAgentAny() + ); + } + + private User buildUser() { + return User.builder() + .id(id) + .email(email) + .password(password) + .nickname(nickname) + .image(image) + .build(); + } + +} diff --git a/src/test/java/com/backendoori/ootw/user/service/UserServiceTest.java b/src/test/java/com/backendoori/ootw/user/service/UserServiceTest.java index 7f36f59f..401f3c7b 100644 --- a/src/test/java/com/backendoori/ootw/user/service/UserServiceTest.java +++ b/src/test/java/com/backendoori/ootw/user/service/UserServiceTest.java @@ -1,17 +1,21 @@ package com.backendoori.ootw.user.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatException; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatNoException; -import com.backendoori.ootw.exception.AlreadyExistEmailException; -import com.backendoori.ootw.exception.IncorrectPasswordException; +import java.util.stream.Stream; import com.backendoori.ootw.exception.UserNotFoundException; import com.backendoori.ootw.user.domain.User; import com.backendoori.ootw.user.dto.LoginDto; import com.backendoori.ootw.user.dto.SignupDto; import com.backendoori.ootw.user.dto.TokenDto; +import com.backendoori.ootw.user.exception.AlreadyExistEmailException; +import com.backendoori.ootw.user.exception.IncorrectPasswordException; import com.backendoori.ootw.user.repository.UserRepository; +import com.backendoori.ootw.user.validation.Message; import net.datafaker.Faker; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.jupiter.api.AfterEach; @@ -21,6 +25,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.security.crypto.password.PasswordEncoder; @@ -73,10 +81,37 @@ void failAlreadyExistUser() { ThrowingCallable signup = () -> userService.signup(signupDto); // then - assertThatExceptionOfType(AlreadyExistEmailException.class).isThrownBy(signup) + assertThatExceptionOfType(AlreadyExistEmailException.class) + .isThrownBy(signup) .withMessage(AlreadyExistEmailException.DEFAULT_MESSAGE); } + @DisplayName("비밀번호 형식이 올바르지 않을 경우 회원가입에 실패한다") + @NullAndEmptySource + @MethodSource("generateInvalidPasswords") + @ParameterizedTest + void failInvalidPassword(String password) { + // given + SignupDto signupDto = generateSignupDto(password); + + // when + ThrowingCallable signup = () -> userService.signup(signupDto); + + // then + assertThatIllegalArgumentException() + .isThrownBy(signup) + .withMessage(Message.INVALID_PASSWORD); + } + + private static Stream generateInvalidPasswords() { + return Stream.of( + faker.internet().password(1, 7, true, true, true), + faker.internet().password(31, 50, true, true, true), + faker.internet().password(8, 30, true, false, true), + faker.internet().password(8, 30, true, true, false) + ); + } + } @DisplayName("로그인 테스트") @@ -95,14 +130,15 @@ void success() { TokenDto tokenDto = userService.login(loginDto); // then - assertThat(tokenDto.token()).isInstanceOf(String.class) + assertThat(tokenDto.token()) + .isInstanceOf(String.class) .isNotNull() .isNotBlank(); } @DisplayName("email이 일치하는 사용자가 없으면 로그인에 실패한다") @Test - void failNotExistUser() { + void failUserNotFound() { // given String password = faker.internet().password(); User user = generateUser(password); @@ -112,7 +148,8 @@ void failNotExistUser() { ThrowingCallable login = () -> userService.login(loginDto); // then - assertThatExceptionOfType(UserNotFoundException.class).isThrownBy(login) + assertThatExceptionOfType(UserNotFoundException.class) + .isThrownBy(login) .withMessage(UserNotFoundException.DEFAULT_MESSAGE); } @@ -128,19 +165,22 @@ void failIncorrectPassword() { ThrowingCallable login = () -> userService.login(loginDto); // then - assertThatExceptionOfType(IncorrectPasswordException.class).isThrownBy(login) + assertThatExceptionOfType(IncorrectPasswordException.class) + .isThrownBy(login) .withMessage(IncorrectPasswordException.DEFAULT_MESSAGE); } } private SignupDto generateSignupDto() { + return generateSignupDto(faker.internet().password(8, 30, true, true, true)); + } + + private SignupDto generateSignupDto(String password) { String email = faker.internet().emailAddress(); - String password = faker.internet().password(); String nickname = faker.internet().username(); - String image = faker.internet().url(); - return new SignupDto(email, password, nickname, image); + return new SignupDto(email, password, nickname); } private User generateUser(String password) {