From c0dcfd8aa69e2c7e316fd19b2c35527541e59906 Mon Sep 17 00:00:00 2001 From: "hanyoung.park" Date: Mon, 2 Feb 2026 01:26:59 +0900 Subject: [PATCH 1/3] remove: deprecated codeguide --- .codeguide/loopers-1-week.md | 45 ------------------------------------ 1 file changed, 45 deletions(-) delete mode 100644 .codeguide/loopers-1-week.md diff --git a/.codeguide/loopers-1-week.md b/.codeguide/loopers-1-week.md deleted file mode 100644 index a8ace53e..00000000 --- a/.codeguide/loopers-1-week.md +++ /dev/null @@ -1,45 +0,0 @@ -## ๐Ÿงช Implementation Quest - -> ์ง€์ •๋œ **๋‹จ์œ„ ํ…Œ์ŠคํŠธ / ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ / E2E ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค**๋ฅผ ํ•„์ˆ˜๋กœ ๊ตฌํ˜„ํ•˜๊ณ , ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋ฅผ ํ†ต๊ณผ์‹œํ‚ค๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค. - -### ํšŒ์› ๊ฐ€์ž… - -**๐Ÿงฑ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ** - -- [ ] ID ๊ฐ€ `์˜๋ฌธ ๋ฐ ์ˆซ์ž 10์ž ์ด๋‚ด` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. -- [ ] ์ด๋ฉ”์ผ์ด `xx@yy.zz` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. -- [ ] ์ƒ๋…„์›”์ผ์ด `yyyy-MM-dd` ํ˜•์‹์— ๋งž์ง€ ์•Š์œผ๋ฉด, User ๊ฐ์ฒด ์ƒ์„ฑ์— ์‹คํŒจํ•œ๋‹ค. - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [ ] ํšŒ์› ๊ฐ€์ž…์‹œ User ์ €์žฅ์ด ์ˆ˜ํ–‰๋œ๋‹ค. ( spy ๊ฒ€์ฆ ) -- [ ] ์ด๋ฏธ ๊ฐ€์ž…๋œ ID ๋กœ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ ์‹œ, ์‹คํŒจํ•œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [ ] ํšŒ์› ๊ฐ€์ž…์ด ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ์ƒ์„ฑ๋œ ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [ ] ํšŒ์› ๊ฐ€์ž… ์‹œ์— ์„ฑ๋ณ„์ด ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - -### ๋‚ด ์ •๋ณด ์กฐํšŒ - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ํšŒ์› ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. -- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [ ] ๋‚ด ์ •๋ณด ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ์œ ์ € ์ •๋ณด๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [ ] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ID ๋กœ ์กฐํšŒํ•  ๊ฒฝ์šฐ, `404 Not Found` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. - -### ํฌ์ธํŠธ ์กฐํšŒ - -**๐Ÿ”— ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ** - -- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค. -- [ ] ํ•ด๋‹น ID ์˜ ํšŒ์›์ด ์กด์žฌํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ, null ์ด ๋ฐ˜ํ™˜๋œ๋‹ค. - -**๐ŸŒ E2E ํ…Œ์ŠคํŠธ** - -- [ ] ํฌ์ธํŠธ ์กฐํšŒ์— ์„ฑ๊ณตํ•  ๊ฒฝ์šฐ, ๋ณด์œ  ํฌ์ธํŠธ๋ฅผ ์‘๋‹ต์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. -- [ ] `X-USER-ID` ํ—ค๋”๊ฐ€ ์—†์„ ๊ฒฝ์šฐ, `400 Bad Request` ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. From b6202267207ec7a261c9b6ba4c4df5c1c5bd29c3 Mon Sep 17 00:00:00 2001 From: junyoungKim Date: Sun, 8 Feb 2026 22:58:53 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EC=B4=88=EA=B8=B0=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/commerce-api/build.gradle.kts | 3 + .../com/loopers/config/SecurityConfig.java | 15 + .../user/controller/UserController.java | 48 ++++ .../java/com/loopers/user/domain/User.java | 62 +++++ .../user/dto/ChangePasswordRequest.java | 9 + .../loopers/user/dto/CreateUserRequest.java | 22 ++ .../loopers/user/dto/CreateUserResponse.java | 21 ++ .../loopers/user/dto/GetMyInfoResponse.java | 19 ++ .../exception/DuplicateLoginIdException.java | 14 + .../exception/GlobalExceptionHandler.java | 50 ++++ .../InvalidCredentialsException.java | 8 + .../user/exception/SamePasswordException.java | 14 + .../user/repository/UserRepository.java | 15 + .../com/loopers/user/service/UserService.java | 77 ++++++ .../user/validator/PasswordValidator.java | 23 ++ .../java/com/loopers/user/UserE2ETest.java | 256 ++++++++++++++++++ .../CreateUserRequestValidationTest.java | 116 ++++++++ .../user/controller/UserControllerTest.java | 126 +++++++++ .../com/loopers/user/domain/UserTest.java | 81 ++++++ .../service/UserServiceIntegrationTest.java | 155 +++++++++++ .../loopers/user/service/UserServiceTest.java | 219 +++++++++++++++ .../user/validator/PasswordValidatorTest.java | 61 +++++ .../com/loopers/config/jpa/JpaConfig.java | 2 +- 23 files changed, 1415 insertions(+), 1 deletion(-) create mode 100644 apps/commerce-api/src/main/java/com/loopers/config/SecurityConfig.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/user/controller/UserController.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/user/domain/User.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/user/dto/ChangePasswordRequest.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/user/dto/CreateUserRequest.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/user/dto/CreateUserResponse.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/user/dto/GetMyInfoResponse.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/user/exception/DuplicateLoginIdException.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/user/exception/GlobalExceptionHandler.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/user/exception/InvalidCredentialsException.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/user/exception/SamePasswordException.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/user/repository/UserRepository.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/user/service/UserService.java create mode 100644 apps/commerce-api/src/main/java/com/loopers/user/validator/PasswordValidator.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/user/UserE2ETest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/user/controller/CreateUserRequestValidationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/user/controller/UserControllerTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/user/domain/UserTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/user/service/UserServiceIntegrationTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/user/service/UserServiceTest.java create mode 100644 apps/commerce-api/src/test/java/com/loopers/user/validator/PasswordValidatorTest.java diff --git a/apps/commerce-api/build.gradle.kts b/apps/commerce-api/build.gradle.kts index 03ce68f0..6acd8606 100644 --- a/apps/commerce-api/build.gradle.kts +++ b/apps/commerce-api/build.gradle.kts @@ -11,6 +11,9 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:${project.properties["springDocOpenApiVersion"]}") + // security + implementation("org.springframework.security:spring-security-crypto") + // querydsl annotationProcessor("com.querydsl:querydsl-apt::jakarta") annotationProcessor("jakarta.persistence:jakarta.persistence-api") diff --git a/apps/commerce-api/src/main/java/com/loopers/config/SecurityConfig.java b/apps/commerce-api/src/main/java/com/loopers/config/SecurityConfig.java new file mode 100644 index 00000000..52b04d23 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/config/SecurityConfig.java @@ -0,0 +1,15 @@ +package com.loopers.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class SecurityConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/user/controller/UserController.java b/apps/commerce-api/src/main/java/com/loopers/user/controller/UserController.java new file mode 100644 index 00000000..b43fe448 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/user/controller/UserController.java @@ -0,0 +1,48 @@ +package com.loopers.user.controller; + +import com.loopers.user.domain.User; +import com.loopers.user.dto.ChangePasswordRequest; +import com.loopers.user.dto.CreateUserRequest; +import com.loopers.user.dto.CreateUserResponse; +import com.loopers.user.dto.GetMyInfoResponse; +import com.loopers.user.service.UserService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/api/v1/users") +public class UserController { + + public static final String LOGIN_ID_HEADER = "X-Loopers-LoginId"; + public static final String LOGIN_PW_HEADER = "X-Loopers-LoginPw"; + + private final UserService userService; + + @PostMapping + public ResponseEntity createUser(@Valid @RequestBody CreateUserRequest request) { + User user = userService.createUser(request); + return ResponseEntity.status(HttpStatus.CREATED).body(CreateUserResponse.from(user)); + } + + @GetMapping("/me") + public ResponseEntity getMyInfo( + @RequestHeader(LOGIN_ID_HEADER) String loginId + ) { + GetMyInfoResponse response = userService.getMyInfo(loginId); + return ResponseEntity.ok(response); + } + + @PatchMapping("/password") + public ResponseEntity changePassword( + @RequestHeader(LOGIN_ID_HEADER) String loginId, + @RequestHeader(LOGIN_PW_HEADER) String currentPassword, + @Valid @RequestBody ChangePasswordRequest request + ) { + userService.changePassword(loginId, currentPassword, request.newPassword()); + return ResponseEntity.ok().build(); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/user/domain/User.java b/apps/commerce-api/src/main/java/com/loopers/user/domain/User.java new file mode 100644 index 00000000..e40a9334 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/user/domain/User.java @@ -0,0 +1,62 @@ +package com.loopers.user.domain; + +import com.loopers.domain.BaseEntity; +import jakarta.persistence.*; +import org.springframework.util.Assert; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "users") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class User extends BaseEntity { + + @Column(nullable = false, length = 10, unique = true) + private String loginId; + + @Column(nullable = false) + private String password; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String birthDate; + + @Column(nullable = false) + private String email; + + @Builder + public User(String loginId, String password, String name, String birthDate, String email) { + validateRequired(loginId, password, name, birthDate, email); + + this.loginId = loginId; + this.password = password; + this.name = name; + this.birthDate = birthDate; + this.email = email; + } + + private void validateRequired(String loginId, String password, String name, String birthDate, String email) { + Assert.hasText(loginId, "loginId๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค"); + Assert.hasText(password, "password๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค"); + Assert.hasText(name, "name์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค"); + Assert.hasText(birthDate, "birthDate๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค"); + Assert.hasText(email, "email์€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค"); + } + + public String getMaskedName() { + if (name.length() == 1) { + return "*"; + } + return name.substring(0, name.length() - 1) + "*"; + } + + public void changePassword(String newPassword) { + Assert.hasText(newPassword, "์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค"); + this.password = newPassword; + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/user/dto/ChangePasswordRequest.java b/apps/commerce-api/src/main/java/com/loopers/user/dto/ChangePasswordRequest.java new file mode 100644 index 00000000..c4b500b7 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/user/dto/ChangePasswordRequest.java @@ -0,0 +1,9 @@ +package com.loopers.user.dto; + +import jakarta.validation.constraints.NotBlank; + +public record ChangePasswordRequest( + @NotBlank + String newPassword +) { +} diff --git a/apps/commerce-api/src/main/java/com/loopers/user/dto/CreateUserRequest.java b/apps/commerce-api/src/main/java/com/loopers/user/dto/CreateUserRequest.java new file mode 100644 index 00000000..2762a54f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/user/dto/CreateUserRequest.java @@ -0,0 +1,22 @@ +package com.loopers.user.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; + +public record CreateUserRequest( + @NotBlank + @Pattern(regexp = "^[a-zA-Z0-9]+$") + String loginId, + @NotBlank + String password, + @NotBlank + String name, + @NotBlank + @Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}") + String birthDate, + @Email + @NotBlank + String email +) { +} diff --git a/apps/commerce-api/src/main/java/com/loopers/user/dto/CreateUserResponse.java b/apps/commerce-api/src/main/java/com/loopers/user/dto/CreateUserResponse.java new file mode 100644 index 00000000..30179061 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/user/dto/CreateUserResponse.java @@ -0,0 +1,21 @@ +package com.loopers.user.dto; + +import com.loopers.user.domain.User; + +public record CreateUserResponse( + Long id, + String loginId, + String name, + String email, + String birthDate +) { + public static CreateUserResponse from(User user) { + return new CreateUserResponse( + user.getId(), + user.getLoginId(), + user.getName(), + user.getEmail(), + user.getBirthDate() + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/user/dto/GetMyInfoResponse.java b/apps/commerce-api/src/main/java/com/loopers/user/dto/GetMyInfoResponse.java new file mode 100644 index 00000000..71023def --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/user/dto/GetMyInfoResponse.java @@ -0,0 +1,19 @@ +package com.loopers.user.dto; + +import com.loopers.user.domain.User; + +public record GetMyInfoResponse( + String loginId, + String name, + String birthDate, + String email +) { + public static GetMyInfoResponse from(User user) { + return new GetMyInfoResponse( + user.getLoginId(), + user.getMaskedName(), + user.getBirthDate(), + user.getEmail() + ); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/user/exception/DuplicateLoginIdException.java b/apps/commerce-api/src/main/java/com/loopers/user/exception/DuplicateLoginIdException.java new file mode 100644 index 00000000..3a837c04 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/user/exception/DuplicateLoginIdException.java @@ -0,0 +1,14 @@ +package com.loopers.user.exception; + +public class DuplicateLoginIdException extends RuntimeException { + + private static final String DEFAULT_MESSAGE = "์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ธ ๋กœ๊ทธ์ธ ID์ž…๋‹ˆ๋‹ค."; + + public DuplicateLoginIdException() { + super(DEFAULT_MESSAGE); + } + + public DuplicateLoginIdException(String message) { + super(message); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/user/exception/GlobalExceptionHandler.java b/apps/commerce-api/src/main/java/com/loopers/user/exception/GlobalExceptionHandler.java new file mode 100644 index 00000000..c5ab863e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/user/exception/GlobalExceptionHandler.java @@ -0,0 +1,50 @@ +package com.loopers.user.exception; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +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.RestControllerAdvice; + +import java.util.Map; + +@RestControllerAdvice +@Order(Ordered.HIGHEST_PRECEDENCE) +public class GlobalExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidation(MethodArgumentNotValidException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .findFirst() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .orElse("Validation failed"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("message", message)); + } + + @ExceptionHandler(DuplicateLoginIdException.class) + public ResponseEntity> handleDuplicateLoginId(DuplicateLoginIdException e) { + return ResponseEntity.status(HttpStatus.CONFLICT) + .body(Map.of("message", e.getMessage())); + } + + @ExceptionHandler(InvalidCredentialsException.class) + public ResponseEntity> handleInvalidCredentials(InvalidCredentialsException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(Map.of("message", e.getMessage())); + } + + @ExceptionHandler(SamePasswordException.class) + public ResponseEntity> handleSamePassword(SamePasswordException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("message", e.getMessage())); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgument(IllegalArgumentException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("message", e.getMessage())); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/user/exception/InvalidCredentialsException.java b/apps/commerce-api/src/main/java/com/loopers/user/exception/InvalidCredentialsException.java new file mode 100644 index 00000000..41773ec4 --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/user/exception/InvalidCredentialsException.java @@ -0,0 +1,8 @@ +package com.loopers.user.exception; + +public class InvalidCredentialsException extends RuntimeException { + + public InvalidCredentialsException() { + super("์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/user/exception/SamePasswordException.java b/apps/commerce-api/src/main/java/com/loopers/user/exception/SamePasswordException.java new file mode 100644 index 00000000..a6a6da7e --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/user/exception/SamePasswordException.java @@ -0,0 +1,14 @@ +package com.loopers.user.exception; + +public class SamePasswordException extends RuntimeException { + + private static final String DEFAULT_MESSAGE = "์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ํ˜„์žฌ ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๋‹ฌ๋ผ์•ผ ํ•ฉ๋‹ˆ๋‹ค."; + + public SamePasswordException() { + super(DEFAULT_MESSAGE); + } + + public SamePasswordException(String message) { + super(message); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/user/repository/UserRepository.java b/apps/commerce-api/src/main/java/com/loopers/user/repository/UserRepository.java new file mode 100644 index 00000000..d307843f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/user/repository/UserRepository.java @@ -0,0 +1,15 @@ +package com.loopers.user.repository; + +import com.loopers.user.domain.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + + boolean existsByLoginId(String loginId); + + Optional findByLoginId(String loginId); +} diff --git a/apps/commerce-api/src/main/java/com/loopers/user/service/UserService.java b/apps/commerce-api/src/main/java/com/loopers/user/service/UserService.java new file mode 100644 index 00000000..3e427dde --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/user/service/UserService.java @@ -0,0 +1,77 @@ +package com.loopers.user.service; + +import com.loopers.user.domain.User; +import com.loopers.user.dto.CreateUserRequest; +import com.loopers.user.dto.GetMyInfoResponse; +import com.loopers.user.exception.DuplicateLoginIdException; +import com.loopers.user.exception.InvalidCredentialsException; +import com.loopers.user.exception.SamePasswordException; +import com.loopers.user.repository.UserRepository; +import com.loopers.user.validator.PasswordValidator; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Service +public class UserService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + @Transactional + public User createUser(CreateUserRequest request) { + + if(userRepository.existsByLoginId(request.loginId())){ + throw new DuplicateLoginIdException(); + } + + //๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ + PasswordValidator.validate(request.password(), request.birthDate()); + + //๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” + String encodedPassword = passwordEncoder.encode(request.password()); + + User user = new User( + request.loginId(), + encodedPassword, + request.name(), + request.birthDate(), + request.email() + ); + + return userRepository.save(user); + } + + @Transactional(readOnly = true) + public GetMyInfoResponse getMyInfo(String loginId) { + User user = userRepository.findByLoginId(loginId) + .orElseThrow(InvalidCredentialsException::new); + + return GetMyInfoResponse.from(user); + } + + @Transactional + public void changePassword(String loginId, String currentPassword, String newPassword) { + User user = userRepository.findByLoginId(loginId) + .orElseThrow(InvalidCredentialsException::new); + + // ๊ธฐ์กด ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ + if (!passwordEncoder.matches(currentPassword, user.getPassword())) { + throw new InvalidCredentialsException(); + } + + // ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๊ธฐ์กด ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ๋™์ผํ•œ์ง€ ํ™•์ธ + if (passwordEncoder.matches(newPassword, user.getPassword())) { + throw new SamePasswordException(); + } + + // ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ทœ์น™ ๊ฒ€์ฆ + PasswordValidator.validate(newPassword, user.getBirthDate()); + + // ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” ํ›„ ์ €์žฅ + String encodedNewPassword = passwordEncoder.encode(newPassword); + user.changePassword(encodedNewPassword); + } +} diff --git a/apps/commerce-api/src/main/java/com/loopers/user/validator/PasswordValidator.java b/apps/commerce-api/src/main/java/com/loopers/user/validator/PasswordValidator.java new file mode 100644 index 00000000..fb72595f --- /dev/null +++ b/apps/commerce-api/src/main/java/com/loopers/user/validator/PasswordValidator.java @@ -0,0 +1,23 @@ +package com.loopers.user.validator; + +public class PasswordValidator { + + private PasswordValidator() { + } + + public static void validate(String password, String birthDate) { + if (password.length() < 8) { + throw new IllegalArgumentException("๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 8์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + if (password.length() > 16) { + throw new IllegalArgumentException("๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” 16์ž ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค."); + } + String pattern = "^[a-zA-Z0-9!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?]+$"; + if (!password.matches(pattern)) { + throw new IllegalArgumentException("๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์˜๋ฌธ ๋Œ€์†Œ๋ฌธ์ž, ์ˆซ์ž, ํŠน์ˆ˜๋ฌธ์ž๋งŒ ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค."); + } + if (birthDate != null && password.contains(birthDate)) { + throw new IllegalArgumentException("๋น„๋ฐ€๋ฒˆํ˜ธ์— ์ƒ๋…„์›”์ผ์„ ํฌํ•จํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + } + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/user/UserE2ETest.java b/apps/commerce-api/src/test/java/com/loopers/user/UserE2ETest.java new file mode 100644 index 00000000..f201ee0d --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/user/UserE2ETest.java @@ -0,0 +1,256 @@ +package com.loopers.user; + +import com.loopers.testcontainers.MySqlTestContainersConfig; +import com.loopers.user.dto.ChangePasswordRequest; +import com.loopers.user.dto.CreateUserRequest; +import com.loopers.user.dto.CreateUserResponse; +import com.loopers.user.dto.GetMyInfoResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.annotation.Import; +import org.springframework.http.*; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.transaction.annotation.Transactional; + +import static com.loopers.user.controller.UserController.LOGIN_ID_HEADER; +import static com.loopers.user.controller.UserController.LOGIN_PW_HEADER; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Import(MySqlTestContainersConfig.class) +@Transactional +public class UserE2ETest { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + void ํšŒ์›๊ฐ€์ž…_API_์š”์ฒญ์‹œ_์‚ฌ์šฉ์ž๊ฐ€_์ƒ์„ฑ๋˜๊ณ _201_Created_๋ฐ˜ํ™˜() { + // given + CreateUserRequest request = new CreateUserRequest( + "testuser", + "Password1!", + "ํ™๊ธธ๋™", + "1990-01-01", + "test@example.com" + ); + + //์‹ค์ œ HTTP ์š”์ฒญ + ResponseEntity response = restTemplate.postForEntity( + "/api/v1/users", + request, + CreateUserResponse.class + ); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + } + + @Test + void ์ด๋ฏธ_์กด์žฌํ•˜๋Š”_๋กœ๊ทธ์ธID๋กœ_ํšŒ์›๊ฐ€์ž…์‹œ_409_Conflict_๋ฐ˜ํ™˜() { + // given + CreateUserRequest request = new CreateUserRequest( + "dupuser", + "Password1!", + "ํ™๊ธธ๋™", + "1990-01-01", + "test@example.com" + ); + + // ์ฒซ ๋ฒˆ์งธ ํšŒ์›๊ฐ€์ž… (์„ฑ๊ณต) + restTemplate.postForEntity("/api/v1/users", request, CreateUserResponse.class); + + // when - ๋™์ผํ•œ loginId๋กœ ๋‘ ๋ฒˆ์งธ ํšŒ์›๊ฐ€์ž… ์‹œ๋„ + ResponseEntity response = restTemplate.postForEntity( + "/api/v1/users", + request, + String.class + ); + + // then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + void ๋‚ด_์ •๋ณด_์กฐํšŒ_API_์š”์ฒญ์‹œ_๋งˆ์Šคํ‚น๋œ_์ด๋ฆ„์ด_ํฌํ•จ๋œ_์‚ฌ์šฉ์ž_์ •๋ณด์™€_200_OK_๋ฐ˜ํ™˜() { + // given - ์‚ฌ์šฉ์ž ์ƒ์„ฑ + String loginId = "myinfouser"; + CreateUserRequest createRequest = new CreateUserRequest( + loginId, "Password1!", "ํ™๊ธธ๋™", "1990-01-01", "test@example.com" + ); + restTemplate.postForEntity("/api/v1/users", createRequest, CreateUserResponse.class); + + // when - ๋‚ด ์ •๋ณด ์กฐํšŒ + HttpHeaders headers = new HttpHeaders(); + headers.set(LOGIN_ID_HEADER, loginId); + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange( + "/api/v1/users/me", + HttpMethod.GET, + entity, + GetMyInfoResponse.class + ); + + // then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().loginId()).isEqualTo(loginId); + assertThat(response.getBody().name()).isEqualTo("ํ™๊ธธ*"); + assertThat(response.getBody().birthDate()).isEqualTo("1990-01-01"); + assertThat(response.getBody().email()).isEqualTo("test@example.com"); + } + + @Test + void ์กด์žฌํ•˜์ง€_์•Š๋Š”_๋กœ๊ทธ์ธID๋กœ_๋‚ด_์ •๋ณด_์กฐํšŒ์‹œ_401_Unauthorized_๋ฐ˜ํ™˜() { + // given - ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋กœ๊ทธ์ธ ID + HttpHeaders headers = new HttpHeaders(); + headers.set(LOGIN_ID_HEADER, "nonexistentuser"); + HttpEntity entity = new HttpEntity<>(headers); + + // when + ResponseEntity response = restTemplate.exchange( + "/api/v1/users/me", + HttpMethod.GET, + entity, + String.class + ); + + // then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void ๋น„๋ฐ€๋ฒˆํ˜ธ_๋ณ€๊ฒฝ_API_์š”์ฒญ์‹œ_200_OK_๋ฐ˜ํ™˜() { + // given - ์‚ฌ์šฉ์ž ์ƒ์„ฑ + String loginId = "pwchg" + (System.currentTimeMillis() % 10000); + String currentPassword = "Password1!"; + String newPassword = "NewPassword2@"; + + CreateUserRequest createRequest = new CreateUserRequest( + loginId, currentPassword, "ํ™๊ธธ๋™", "1990-01-01", "test@example.com" + ); + ResponseEntity createResponse = restTemplate.postForEntity("/api/v1/users", createRequest, CreateUserResponse.class); + assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED); + + // when - ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ + HttpHeaders headers = new HttpHeaders(); + headers.set(LOGIN_ID_HEADER, loginId); + headers.set(LOGIN_PW_HEADER, currentPassword); + headers.setContentType(MediaType.APPLICATION_JSON); + + ChangePasswordRequest changeRequest = new ChangePasswordRequest(newPassword); + HttpEntity entity = new HttpEntity<>(changeRequest, headers); + + ResponseEntity response = restTemplate.exchange( + "/api/v1/users/password", + HttpMethod.PATCH, + entity, + Void.class + ); + + // then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void ๋น„๋ฐ€๋ฒˆํ˜ธ_๋ณ€๊ฒฝ์‹œ_๊ธฐ์กด_๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€_์ผ์น˜ํ•˜์ง€_์•Š์œผ๋ฉด_401_Unauthorized_๋ฐ˜ํ™˜() { + // given - ์‚ฌ์šฉ์ž ์ƒ์„ฑ + String loginId = "pwfail" + (System.currentTimeMillis() % 1000); + String currentPassword = "Password1!"; + String wrongPassword = "WrongPassword!"; + String newPassword = "NewPassword2@"; + + CreateUserRequest createRequest = new CreateUserRequest( + loginId, currentPassword, "ํ™๊ธธ๋™", "1990-01-01", "test@example.com" + ); + ResponseEntity createResponse = restTemplate.postForEntity("/api/v1/users", createRequest, CreateUserResponse.class); + assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED); + + // when - ์ž˜๋ชป๋œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋ณ€๊ฒฝ ์‹œ๋„ + HttpHeaders headers = new HttpHeaders(); + headers.set(LOGIN_ID_HEADER, loginId); + headers.set(LOGIN_PW_HEADER, wrongPassword); + headers.setContentType(MediaType.APPLICATION_JSON); + + ChangePasswordRequest changeRequest = new ChangePasswordRequest(newPassword); + HttpEntity entity = new HttpEntity<>(changeRequest, headers); + + ResponseEntity response = restTemplate.exchange( + "/api/v1/users/password", + HttpMethod.PATCH, + entity, + String.class + ); + + // then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void ๋น„๋ฐ€๋ฒˆํ˜ธ_๋ณ€๊ฒฝ์‹œ_์ƒˆ_๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€_๊ธฐ์กด๊ณผ_๋™์ผํ•˜๋ฉด_400_Bad_Request_๋ฐ˜ํ™˜() { + // given - ์‚ฌ์šฉ์ž ์ƒ์„ฑ + String loginId = "pwsame" + (System.currentTimeMillis() % 1000); + String currentPassword = "Password1!"; + + CreateUserRequest createRequest = new CreateUserRequest( + loginId, currentPassword, "ํ™๊ธธ๋™", "1990-01-01", "test@example.com" + ); + ResponseEntity createResponse = restTemplate.postForEntity("/api/v1/users", createRequest, CreateUserResponse.class); + assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED); + + // when - ๋™์ผํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋ณ€๊ฒฝ ์‹œ๋„ + HttpHeaders headers = new HttpHeaders(); + headers.set(LOGIN_ID_HEADER, loginId); + headers.set(LOGIN_PW_HEADER, currentPassword); + headers.setContentType(MediaType.APPLICATION_JSON); + + ChangePasswordRequest changeRequest = new ChangePasswordRequest(currentPassword); + HttpEntity entity = new HttpEntity<>(changeRequest, headers); + + ResponseEntity response = restTemplate.exchange( + "/api/v1/users/password", + HttpMethod.PATCH, + entity, + String.class + ); + + // then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + void ๋น„๋ฐ€๋ฒˆํ˜ธ_๋ณ€๊ฒฝ์‹œ_8์ž_๋ฏธ๋งŒ_๊ทœ์น™_์œ„๋ฐ˜ํ•˜๋ฉด_400_Bad_Request_๋ฐ˜ํ™˜() { + // given - ์‚ฌ์šฉ์ž ์ƒ์„ฑ + String loginId = "pwrule" + (System.currentTimeMillis() % 1000); + String currentPassword = "Password1!"; + String invalidPassword = "short"; // 8์ž ๋ฏธ๋งŒ + + CreateUserRequest createRequest = new CreateUserRequest( + loginId, currentPassword, "ํ™๊ธธ๋™", "1990-01-01", "test@example.com" + ); + ResponseEntity createResponse = restTemplate.postForEntity("/api/v1/users", createRequest, CreateUserResponse.class); + assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED); + + // when - ๊ทœ์น™ ์œ„๋ฐ˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋ณ€๊ฒฝ ์‹œ๋„ + HttpHeaders headers = new HttpHeaders(); + headers.set(LOGIN_ID_HEADER, loginId); + headers.set(LOGIN_PW_HEADER, currentPassword); + headers.setContentType(MediaType.APPLICATION_JSON); + + ChangePasswordRequest changeRequest = new ChangePasswordRequest(invalidPassword); + HttpEntity entity = new HttpEntity<>(changeRequest, headers); + + ResponseEntity response = restTemplate.exchange( + "/api/v1/users/password", + HttpMethod.PATCH, + entity, + String.class + ); + + // then + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/user/controller/CreateUserRequestValidationTest.java b/apps/commerce-api/src/test/java/com/loopers/user/controller/CreateUserRequestValidationTest.java new file mode 100644 index 00000000..d861bce2 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/user/controller/CreateUserRequestValidationTest.java @@ -0,0 +1,116 @@ +package com.loopers.user.controller; + +import com.loopers.user.dto.CreateUserRequest; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.Pattern; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Set; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CreateUserRequestValidationTest { + + private Validator validator; + + @BeforeEach + void setUp() { + validator = Validation.buildDefaultValidatorFactory().getValidator(); + } + + @ParameterizedTest + @MethodSource("ํ•„์ˆ˜๊ฐ’_๋ˆ„๋ฝ_์ผ€์ด์Šค") + void ํšŒ์›๊ฐ€์ž…์‹œ_ํ•„์ˆ˜์ •๋ณด๋ฅผ_์ž…๋ ฅํ•˜์ง€_์•Š์œผ๋ฉด_์‹คํŒจํ•œ๋‹ค(CreateUserRequest request, String expectedField) { + //given + + //when + Set> violations = validator.validate(request); + + //then + assertThat(violations).hasSize(1); + assertThat(violations.iterator().next().getPropertyPath().toString()).isEqualTo(expectedField); + } + + static Stream ํ•„์ˆ˜๊ฐ’_๋ˆ„๋ฝ_์ผ€์ด์Šค() { + return Stream.of( + Arguments.of(new CreateUserRequest(null, "pw", "name", "1990-01-01", "a@a.com"), "loginId"), + Arguments.of(new CreateUserRequest("test", null, "name", "1990-01-01", "a@a.com"), "password"), + Arguments.of(new CreateUserRequest("test", "pw", null, "1990-01-01", "a@a.com"), "name"), + Arguments.of(new CreateUserRequest("test", "pw", "name", null, "a@a.com"), "birthDate"), + Arguments.of(new CreateUserRequest("test", "pw", "name", "1990-01-01", null), "email") + ); + } + + @Test + void ์ด๋ฉ”์ผ_ํ˜•์‹_๋ถˆ์ผ์น˜_์‹œ_์‹คํŒจ() { + //given + String id = "test"; + String password = "pw"; + String name = "name"; + String birthDate = "1990-01-01"; + String email = "test123"; + CreateUserRequest request = new CreateUserRequest(id, password, name, birthDate, email); + + //when + Set> violations = validator.validate(request); + + //then + assertThat(violations).hasSize(1); + ConstraintViolation violation = violations.iterator().next(); + assertThat(violation.getPropertyPath().toString()).isEqualTo("email"); + assertThat(violation.getConstraintDescriptor() + .getAnnotation() + .annotationType()) + .isEqualTo(Email.class); + } + + @Test + void ์ƒ๋…„์›”์ผ_ํ˜•์‹_๋ถˆ์ผ์น˜_์‹œ_์‹คํŒจ() { + //given + String id = "test"; + String password = "pw"; + String name = "name"; + String birthDate = "19900427"; + String email = "test123@test.com"; + CreateUserRequest request = new CreateUserRequest(id, password, name, birthDate, email); + + //when + Set> violations = validator.validate(request); + + //then + assertThat(violations).hasSize(1); + ConstraintViolation violation = violations.iterator().next(); + assertThat(violation.getPropertyPath().toString()).isEqualTo("birthDate"); + } + + @Test + void ๋กœ๊ทธ์ธID์—_์˜๋ฌธ_์ˆซ์ž_์™ธ_๋ฌธ์ž_ํฌํ•จ_์‹œ_์‹คํŒจ() { + //given + String id = "test@123"; + String password = "pw"; + String name = "name"; + String birthDate = "1990-01-01"; + String email = "test@test.com"; + CreateUserRequest request = new CreateUserRequest(id, password, name, birthDate, email); + + //when + Set> violations = validator.validate(request); + + //then + assertThat(violations).hasSize(1); + ConstraintViolation violation = violations.iterator().next(); + assertThat(violation.getPropertyPath().toString()).isEqualTo("loginId"); + assertThat(violation.getConstraintDescriptor() + .getAnnotation() + .annotationType()) + .isEqualTo(Pattern.class); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/user/controller/UserControllerTest.java b/apps/commerce-api/src/test/java/com/loopers/user/controller/UserControllerTest.java new file mode 100644 index 00000000..a82ce7c4 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/user/controller/UserControllerTest.java @@ -0,0 +1,126 @@ +package com.loopers.user.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.loopers.user.domain.User; +import com.loopers.user.dto.CreateUserRequest; +import com.loopers.user.exception.GlobalExceptionHandler; +import com.loopers.user.service.UserService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.util.stream.Stream; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(UserController.class) +@Import(GlobalExceptionHandler.class) +public class UserControllerTest { + @Autowired + MockMvc mockMvc; + + @Autowired + ObjectMapper objectMapper; + + @MockitoBean + UserService userService; + + @Test + void ํšŒ์›๊ฐ€์ž…_์„ฑ๊ณต_์‹œ_201_๋ฐ˜ํ™˜() throws Exception { + //given + CreateUserRequest request = new CreateUserRequest( + "testId", "password123!", "๊น€์ค€์˜", "1990-04-27", "test@test.com" + ); + + User user = User.builder() + .loginId("testId") + .password("encoded") + .name("๊น€์ค€์˜") + .birthDate("1990-04-27") + .email("test@test.com") + .build(); + + given(userService.createUser(any(CreateUserRequest.class))) + .willReturn(user); + + //when + ResultActions result = mockMvc.perform(post("/api/v1/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + //then + result.andExpect(status().isCreated()); + } + + @ParameterizedTest(name = "{1} ๋ˆ„๋ฝ ์‹œ 400 ๋ฐ˜ํ™˜") + @MethodSource("ํ•„์ˆ˜๊ฐ’_๋ˆ„๋ฝ_์ผ€์ด์Šค") + void ํ•„์ˆ˜๊ฐ’_๋ˆ„๋ฝ_์‹œ_400_Bad_Request_๋ฐ˜ํ™˜(CreateUserRequest request, String fieldName) throws Exception { + //given + + //when + ResultActions result = mockMvc.perform(post("/api/v1/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + //then + result.andExpect(status().isBadRequest()); + } + + static Stream ํ•„์ˆ˜๊ฐ’_๋ˆ„๋ฝ_์ผ€์ด์Šค() { + return Stream.of( + Arguments.of(new CreateUserRequest(null, "pw", "name", "1990-01-01", "a@a.com"), "loginId"), + Arguments.of(new CreateUserRequest("test", null, "name", "1990-01-01", "a@a.com"), "password"), + Arguments.of(new CreateUserRequest("test", "pw", null, "1990-01-01", "a@a.com"), "name"), + Arguments.of(new CreateUserRequest("test", "pw", "name", null, "a@a.com"), "birthDate"), + Arguments.of(new CreateUserRequest("test", "pw", "name", "1990-01-01", null), "email") + ); + } + + @Test + void ์ด๋ฉ”์ผ_ํ˜•์‹_์˜ค๋ฅ˜_์‹œ_400_Bad_Request_๋ฐ˜ํ™˜() throws Exception { + //given + CreateUserRequest request = new CreateUserRequest( + "testId", "password123!", "๊น€์ค€์˜", "1990-04-27", "testtest.com" + ); + + //when + ResultActions result = mockMvc.perform(post("/api/v1/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + //then + result.andDo(print()).andExpect(status().isBadRequest()); + } + + @Test + void ์ƒ๋…„์›”์ผ_ํ˜•์‹_์˜ค๋ฅ˜_์‹œ_400_Bad_Request_๋ฐ˜ํ™˜() throws Exception { + //given + CreateUserRequest request = new CreateUserRequest( + "testId", "password123!", "๊น€์ค€์˜", "1990-0427", "test@test.com" + ); + + //when + ResultActions result = mockMvc.perform(post("/api/v1/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ); + + //then + result.andExpect(status().isBadRequest()); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/user/domain/UserTest.java b/apps/commerce-api/src/test/java/com/loopers/user/domain/UserTest.java new file mode 100644 index 00000000..29f6c521 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/user/domain/UserTest.java @@ -0,0 +1,81 @@ +package com.loopers.user.domain; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class UserTest { + + @Test + void ์ด๋ฆ„์˜_๋งˆ์ง€๋ง‰_๊ธ€์ž๊ฐ€_๋งˆ์Šคํ‚น๋œ๋‹ค() { + // given + User user = User.builder() + .loginId("testId") + .password("password123!") + .name("ํ™๊ธธ๋™") + .birthDate("1990-01-01") + .email("test@test.com") + .build(); + + // when + String maskedName = user.getMaskedName(); + + // then + assertThat(maskedName).isEqualTo("ํ™๊ธธ*"); + } + + @Test + void ํ•œ_๊ธ€์ž_์ด๋ฆ„์€_๋งˆ์Šคํ‚น_๋ฌธ์ž๋กœ_๋ฐ˜ํ™˜๋œ๋‹ค() { + // given + User user = User.builder() + .loginId("testId") + .password("password123!") + .name("๊น€") + .birthDate("1990-01-01") + .email("test@test.com") + .build(); + + // when + String maskedName = user.getMaskedName(); + + // then + assertThat(maskedName).isEqualTo("*"); + } + + @Test + void ๋‘_๊ธ€์ž_์ด๋ฆ„์˜_๋งˆ์ง€๋ง‰_๊ธ€์ž๊ฐ€_๋งˆ์Šคํ‚น๋œ๋‹ค() { + // given + User user = User.builder() + .loginId("testId") + .password("password123!") + .name("์ด์ˆœ") + .birthDate("1990-01-01") + .email("test@test.com") + .build(); + + // when + String maskedName = user.getMaskedName(); + + // then + assertThat(maskedName).isEqualTo("์ด*"); + } + + @Test + void changePassword๋กœ_๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€_๋ณ€๊ฒฝ๋œ๋‹ค() { + // given + User user = User.builder() + .loginId("testId") + .password("oldPassword123!") + .name("ํ™๊ธธ๋™") + .birthDate("1990-01-01") + .email("test@test.com") + .build(); + String newPassword = "newPassword456!"; + + // when + user.changePassword(newPassword); + + // then + assertThat(user.getPassword()).isEqualTo(newPassword); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/user/service/UserServiceIntegrationTest.java b/apps/commerce-api/src/test/java/com/loopers/user/service/UserServiceIntegrationTest.java new file mode 100644 index 00000000..8e5721a2 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/user/service/UserServiceIntegrationTest.java @@ -0,0 +1,155 @@ +package com.loopers.user.service; + +import com.loopers.testcontainers.MySqlTestContainersConfig; +import com.loopers.user.domain.User; +import com.loopers.user.dto.CreateUserRequest; +import com.loopers.user.dto.GetMyInfoResponse; +import com.loopers.user.exception.DuplicateLoginIdException; +import com.loopers.user.exception.InvalidCredentialsException; +import com.loopers.user.exception.SamePasswordException; +import org.springframework.security.crypto.password.PasswordEncoder; +import com.loopers.user.repository.UserRepository; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.transaction.annotation.Transactional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.catchThrowable; + +@SpringBootTest +@Import(MySqlTestContainersConfig.class) +@Transactional +public class UserServiceIntegrationTest { + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserService userService; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Test + void ํšŒ์›_๊ฐ€์ž…์‹œ_User_์ €์žฅ์ด_์ˆ˜ํ–‰๋œ๋‹ค() { + //given + CreateUserRequest request = new CreateUserRequest( + "testuser", "password123!", "ํ™๊ธธ๋™", "1990-04-27", "test@test.com" + ); + + //when + User savedUser = userService.createUser(request); + + //then + User foundUser = userRepository.findById(savedUser.getId()).orElseThrow(); + assertThat(foundUser.getLoginId()).isEqualTo(request.loginId()); + assertThat(foundUser.getName()).isEqualTo(request.name()); + assertThat(foundUser.getEmail()).isEqualTo(request.email()); + } + + @Test + void ์ด๋ฏธ_๊ฐ€์ž…๋œ_ID๋กœ_ํšŒ์›๊ฐ€์ž…_์‹œ๋„_์‹œ_DuplicateLoginIdException์ด_๋ฐœ์ƒํ•œ๋‹ค() { + //given + CreateUserRequest request = new CreateUserRequest( + "testuser", "password123!", "ํ™๊ธธ๋™", "1990-04-27", "test@test.com" + ); + //testuser ๋ผ๋Š” ID๋กœ ๊ฐ€์ž… + userService.createUser(request); + + //when + //๋™์ผํ•œ ์•„์ด๋””๋กœ ๊ฐ€์ž…ํ•˜๋Š” ๊ฒฝ์šฐ + CreateUserRequest duplicateRequest = new CreateUserRequest( + "testuser", "password456!", "๊น€์ฒ ์ˆ˜", "1995-01-01", "other@test.com" + ); + Throwable thrown = catchThrowable(() -> userService.createUser(duplicateRequest)); + + //then + assertThat(thrown).isInstanceOf(DuplicateLoginIdException.class); + } + + @Test + void DB์—_์ €์žฅ๋œ_์‚ฌ์šฉ์ž_์ •๋ณด๋ฅผ_์ •์ƒ์ ์œผ๋กœ_์กฐํšŒํ•œ๋‹ค() { + // given + String loginId = "testuser"; + CreateUserRequest request = new CreateUserRequest( + loginId, "password123!", "ํ™๊ธธ๋™", "1990-01-01", "test@test.com" + ); + userService.createUser(request); + + // when + GetMyInfoResponse response = userService.getMyInfo(loginId); + + // then + assertThat(response.loginId()).isEqualTo(loginId); + assertThat(response.name()).isEqualTo("ํ™๊ธธ*"); + assertThat(response.birthDate()).isEqualTo("1990-01-01"); + assertThat(response.email()).isEqualTo("test@test.com"); + } + + @Test + void ์กด์žฌํ•˜์ง€_์•Š๋Š”_๋กœ๊ทธ์ธID๋กœ_์กฐํšŒ์‹œ_InvalidCredentialsException์ด_๋ฐœ์ƒํ•œ๋‹ค() { + // given + String nonExistentLoginId = "nonexistent"; + + // when & then + assertThatThrownBy(() -> userService.getMyInfo(nonExistentLoginId)) + .isInstanceOf(InvalidCredentialsException.class); + } + + @Test + void DB์—_์ €์žฅ๋œ_์‚ฌ์šฉ์ž์˜_๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€_์ •์ƒ์ ์œผ๋กœ_๋ณ€๊ฒฝ๋œ๋‹ค() { + // given + String loginId = "testuser"; + String currentPassword = "password123!"; + String newPassword = "newPassword456!"; + + CreateUserRequest request = new CreateUserRequest( + loginId, currentPassword, "ํ™๊ธธ๋™", "1990-01-01", "test@test.com" + ); + userService.createUser(request); + + // when + userService.changePassword(loginId, currentPassword, newPassword); + + // then + User updatedUser = userRepository.findByLoginId(loginId).orElseThrow(); + assertThat(passwordEncoder.matches(newPassword, updatedUser.getPassword())).isTrue(); + } + + @Test + void ๋น„๋ฐ€๋ฒˆํ˜ธ_๋ณ€๊ฒฝ์‹œ_๊ธฐ์กด_๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€_์ผ์น˜ํ•˜์ง€_์•Š์œผ๋ฉด_InvalidCredentialsException์ด_๋ฐœ์ƒํ•œ๋‹ค() { + // given + String loginId = "testuser"; + String currentPassword = "password123!"; + String wrongPassword = "wrongPassword!"; + String newPassword = "newPassword456!"; + + CreateUserRequest request = new CreateUserRequest( + loginId, currentPassword, "ํ™๊ธธ๋™", "1990-01-01", "test@test.com" + ); + userService.createUser(request); + + // when & then + assertThatThrownBy(() -> userService.changePassword(loginId, wrongPassword, newPassword)) + .isInstanceOf(InvalidCredentialsException.class); + } + + @Test + void ๋น„๋ฐ€๋ฒˆํ˜ธ_๋ณ€๊ฒฝ์‹œ_์ƒˆ_๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€_๊ธฐ์กด๊ณผ_๋™์ผํ•˜๋ฉด_SamePasswordException์ด_๋ฐœ์ƒํ•œ๋‹ค() { + // given + String loginId = "testuser"; + String currentPassword = "password123!"; + + CreateUserRequest request = new CreateUserRequest( + loginId, currentPassword, "ํ™๊ธธ๋™", "1990-01-01", "test@test.com" + ); + userService.createUser(request); + + // when & then + assertThatThrownBy(() -> userService.changePassword(loginId, currentPassword, currentPassword)) + .isInstanceOf(SamePasswordException.class); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/user/service/UserServiceTest.java b/apps/commerce-api/src/test/java/com/loopers/user/service/UserServiceTest.java new file mode 100644 index 00000000..c0e06360 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/user/service/UserServiceTest.java @@ -0,0 +1,219 @@ +package com.loopers.user.service; + +import com.loopers.user.domain.User; +import com.loopers.user.dto.CreateUserRequest; +import com.loopers.user.dto.GetMyInfoResponse; +import com.loopers.user.exception.InvalidCredentialsException; +import com.loopers.user.exception.SamePasswordException; +import com.loopers.user.repository.UserRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class UserServiceTest { + + @Mock + private UserRepository userRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @InjectMocks + private UserService userService; + + @Test + void ์ •์ƒ_์ž…๋ ฅ์‹œ_ํšŒ์›๊ฐ€์ž…_์„ฑ๊ณต() { + //given + CreateUserRequest request = new CreateUserRequest( + "testId", "password123!", "๊น€์ค€์˜", "1990-04-27", "test@test.com" + ); + + given(passwordEncoder.encode(request.password())).willReturn("encodedPassword"); + given(userRepository.save(any(User.class))) + .willAnswer(invocation -> invocation.getArgument(0)); + + //when + User user = userService.createUser(request); + + //then + assertThat(user.getLoginId()).isEqualTo(request.loginId()); + verify(userRepository).save(any(User.class)); + } + + @Test + void ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€_์•”ํ˜ธํ™”๋˜์–ด_์ €์žฅ๋œ๋‹ค() { + //given + String rawPassword = "password123!"; + String encodedPassword = "encoded_password_hash"; + CreateUserRequest request = new CreateUserRequest( + "testId", rawPassword, "test", "1990-04-27", "test@test.com" + ); + + //password๋ฅผ ์•”ํ˜ธํ™”ํ•œ๋‹ค. + given(passwordEncoder.encode(rawPassword)).willReturn(encodedPassword); + //์‚ฌ์šฉ์ž๋ฅผ ์ €์žฅ์†Œ์— ์ €์žฅํ•œ๋‹ค. + given(userRepository.save(any(User.class))) + .willAnswer(invocation -> invocation.getArgument(0)); + + //when + //ํšŒ์›๊ฐ€์ž…์„ ์ง„ํ–‰ํ–ˆ์„ ๋•Œ + User user = userService.createUser(request); + + //then + //๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์•”ํ˜ธํ™”๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค. + assertThat(user.getPassword()).isEqualTo(encodedPassword); + //์•”ํ˜ธํ™” ๋กœ์ง ํ˜ธ์ถœํ–ˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค. + verify(passwordEncoder).encode(rawPassword); + } + + @Test + void ์œ ํšจํ•œ_๋กœ๊ทธ์ธID๋กœ_๋‚ด_์ •๋ณด๋ฅผ_์กฐํšŒํ•œ๋‹ค() { + // given + String loginId = "testId"; + User user = User.builder() + .loginId(loginId) + .password("encodedPassword") + .name("ํ™๊ธธ๋™") + .birthDate("1990-01-01") + .email("test@test.com") + .build(); + + given(userRepository.findByLoginId(loginId)).willReturn(Optional.of(user)); + + // when + GetMyInfoResponse response = userService.getMyInfo(loginId); + + // then + assertThat(response.loginId()).isEqualTo(loginId); + assertThat(response.name()).isEqualTo("ํ™๊ธธ*"); + assertThat(response.birthDate()).isEqualTo("1990-01-01"); + assertThat(response.email()).isEqualTo("test@test.com"); + } + + @Test + void ์กด์žฌํ•˜์ง€_์•Š๋Š”_๋กœ๊ทธ์ธID๋กœ_์กฐํšŒ์‹œ_InvalidCredentialsException์ด_๋ฐœ์ƒํ•œ๋‹ค() { + // given + String loginId = "nonExistentId"; + + given(userRepository.findByLoginId(loginId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> userService.getMyInfo(loginId)) + .isInstanceOf(InvalidCredentialsException.class); + } + + @Test + void ์˜ฌ๋ฐ”๋ฅธ_๊ธฐ์กด_๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ_๋น„๋ฐ€๋ฒˆํ˜ธ_๋ณ€๊ฒฝ์—_์„ฑ๊ณตํ•œ๋‹ค() { + // given + String loginId = "testId"; + String currentPassword = "oldPassword123!"; + String newPassword = "newPassword456!"; + String encodedCurrentPassword = "encodedOldPassword"; + String encodedNewPassword = "encodedNewPassword"; + + User user = User.builder() + .loginId(loginId) + .password(encodedCurrentPassword) + .name("ํ™๊ธธ๋™") + .birthDate("1990-01-01") + .email("test@test.com") + .build(); + + given(userRepository.findByLoginId(loginId)).willReturn(Optional.of(user)); + given(passwordEncoder.matches(currentPassword, encodedCurrentPassword)).willReturn(true); + given(passwordEncoder.matches(newPassword, encodedCurrentPassword)).willReturn(false); + given(passwordEncoder.encode(newPassword)).willReturn(encodedNewPassword); + + // when + userService.changePassword(loginId, currentPassword, newPassword); + + // then + assertThat(user.getPassword()).isEqualTo(encodedNewPassword); + } + + @Test + void ๊ธฐ์กด_๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€_์ผ์น˜ํ•˜์ง€_์•Š์œผ๋ฉด_InvalidCredentialsException์ด_๋ฐœ์ƒํ•œ๋‹ค() { + // given + String loginId = "testId"; + String wrongPassword = "wrongPassword!"; + String newPassword = "newPassword456!"; + String encodedSavedPassword = "encodedPassword"; + + User user = User.builder() + .loginId(loginId) + .password(encodedSavedPassword) + .name("ํ™๊ธธ๋™") + .birthDate("1990-01-01") + .email("test@test.com") + .build(); + + given(userRepository.findByLoginId(loginId)).willReturn(Optional.of(user)); + given(passwordEncoder.matches(wrongPassword, encodedSavedPassword)).willReturn(false); + + // when & then + assertThatThrownBy(() -> userService.changePassword(loginId, wrongPassword, newPassword)) + .isInstanceOf(InvalidCredentialsException.class); + } + + @Test + void ์ƒˆ_๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€_๊ธฐ์กด_๋น„๋ฐ€๋ฒˆํ˜ธ์™€_๋™์ผํ•˜๋ฉด_SamePasswordException์ด_๋ฐœ์ƒํ•œ๋‹ค() { + // given + String loginId = "testId"; + String currentPassword = "samePassword123!"; + String newPassword = "samePassword123!"; + String encodedSavedPassword = "encodedPassword"; + + User user = User.builder() + .loginId(loginId) + .password(encodedSavedPassword) + .name("ํ™๊ธธ๋™") + .birthDate("1990-01-01") + .email("test@test.com") + .build(); + + given(userRepository.findByLoginId(loginId)).willReturn(Optional.of(user)); + given(passwordEncoder.matches(currentPassword, encodedSavedPassword)).willReturn(true); + given(passwordEncoder.matches(newPassword, encodedSavedPassword)).willReturn(true); + + // when & then + assertThatThrownBy(() -> userService.changePassword(loginId, currentPassword, newPassword)) + .isInstanceOf(SamePasswordException.class); + } + + @Test + void ์ƒˆ_๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€_๊ทœ์น™์—_๋งž์ง€_์•Š์œผ๋ฉด_IllegalArgumentException์ด_๋ฐœ์ƒํ•œ๋‹ค() { + // given + String loginId = "testId"; + String currentPassword = "oldPassword123!"; + String invalidNewPassword = "short"; // 8์ž ๋ฏธ๋งŒ + String encodedSavedPassword = "encodedPassword"; + + User user = User.builder() + .loginId(loginId) + .password(encodedSavedPassword) + .name("ํ™๊ธธ๋™") + .birthDate("1990-01-01") + .email("test@test.com") + .build(); + + given(userRepository.findByLoginId(loginId)).willReturn(Optional.of(user)); + given(passwordEncoder.matches(currentPassword, encodedSavedPassword)).willReturn(true); + given(passwordEncoder.matches(invalidNewPassword, encodedSavedPassword)).willReturn(false); + + // when & then + assertThatThrownBy(() -> userService.changePassword(loginId, currentPassword, invalidNewPassword)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/apps/commerce-api/src/test/java/com/loopers/user/validator/PasswordValidatorTest.java b/apps/commerce-api/src/test/java/com/loopers/user/validator/PasswordValidatorTest.java new file mode 100644 index 00000000..682fad43 --- /dev/null +++ b/apps/commerce-api/src/test/java/com/loopers/user/validator/PasswordValidatorTest.java @@ -0,0 +1,61 @@ +package com.loopers.user.validator; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +public class PasswordValidatorTest { + + + @Test + void ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€_8์ž_๋ฏธ๋งŒ์ด๋ฉด_IllegalArgumentException์ด_๋ฐœ์ƒํ•œ๋‹ค() { + //given + String password = "1234"; + + //when + Throwable thrown = catchThrowable(() -> PasswordValidator.validate(password, null)); + + //then + assertThat(thrown).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€_16์ž_์ดˆ๊ณผํ•˜๋ฉด_IllegalArgumentException์ด_๋ฐœ์ƒํ•œ๋‹ค() { + //given + String password = "12345678901234567"; + + //when + Throwable thrown = catchThrowable(() -> PasswordValidator.validate(password, null)); + + //then + assertThat(thrown).isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @ValueSource(strings = {"Password1!ํ•œ๊ธ€", "Password1!๐Ÿ˜€", "Password 1!"}) + void ๋น„๋ฐ€๋ฒˆํ˜ธ์—_ํ—ˆ์šฉ๋˜์ง€_์•Š๋Š”_๋ฌธ์ž_ํฌํ•จ์‹œ_IllegalArgumentException์ด_๋ฐœ์ƒํ•œ๋‹ค(String password) { + //given + + //when + Throwable thrown = catchThrowable(() -> PasswordValidator.validate(password, null)); + + //then + assertThat(thrown).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void ๋น„๋ฐ€๋ฒˆํ˜ธ์—_์ƒ๋…„์›”์ผ_ํฌํ•จ์‹œ_IllegalArgumentException์ด_๋ฐœ์ƒํ•œ๋‹ค() { + //given + String birthDate = "1990-04-27"; + String password = "pass1990-04-27"; + + //when + Throwable thrown = catchThrowable(() -> PasswordValidator.validate(password, birthDate)); + + //then + assertThat(thrown).isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/modules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.java b/modules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.java index 7fad5872..ef0dc2e4 100644 --- a/modules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.java +++ b/modules/jpa/src/main/java/com/loopers/config/jpa/JpaConfig.java @@ -8,6 +8,6 @@ @Configuration @EnableTransactionManagement @EntityScan({"com.loopers"}) -@EnableJpaRepositories({"com.loopers.infrastructure"}) +@EnableJpaRepositories({"com.loopers.infrastructure", "com.loopers.user.repository"}) public class JpaConfig { } From 641c967840db7f657e0b82b50793f2a5335fad9a Mon Sep 17 00:00:00 2001 From: junyoungKim Date: Thu, 12 Feb 2026 00:45:52 +0900 Subject: [PATCH 3/3] =?UTF-8?q?doc:=20=EC=9A=94=EA=B5=AC=EC=82=AC=ED=95=AD?= =?UTF-8?q?=20=EB=B6=84=EC=84=9D=20skill=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/skills/requirements-analysis/SKILL.md | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .claude/skills/requirements-analysis/SKILL.md diff --git a/.claude/skills/requirements-analysis/SKILL.md b/.claude/skills/requirements-analysis/SKILL.md new file mode 100644 index 00000000..3485a8af --- /dev/null +++ b/.claude/skills/requirements-analysis/SKILL.md @@ -0,0 +1,77 @@ +--- +name: requirements-analysis +description: + ์ œ๊ณต๋œ ์š”๊ตฌ์‚ฌํ•ญ์„ ๋ถ„์„ํ•˜๊ณ , ๊ฐœ๋ฐœ์ž์™€์˜ ์งˆ๋ฌธ/๋Œ€๋‹ต์„ ํ†ตํ•ด ์• ๋งคํ•œ ์š”๊ตฌ์‚ฌํ•ญ์„ ๋ช…ํ™•ํžˆ ํ•˜์—ฌ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + ๋ชจ๋“  ์ •๋ฆฌ๊ฐ€ ๋๋‚˜๋ฉด, ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ, ํด๋ž˜์Šค ๋‹ค์ด์–ด๊ทธ๋žจ, ERD ๋“ฑ์„ Mermaid ๋ฌธ๋ฒ•์œผ๋กœ ์ž‘์„ฑํ•œ๋‹ค. + ์š”๊ตฌ์‚ฌํ•ญ์ด ์ œ๊ณต๋˜์—ˆ์„ ๋•Œ, ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์ „ ์ด๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๋Š” ๋ฐ์— ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. +--- +์š”๊ตฌ์‚ฌํ•ญ์„ ๋ถ„์„ํ•  ๋•Œ ๋ฐ˜๋“œ์‹œ ๋‹ค์Œ ํ๋ฆ„์„ ๋”ฐ๋ฅธ๋‹ค. +### 1๏ธโƒฃ ์š”๊ตฌ์‚ฌํ•ญ์„ ๊ทธ๋Œ€๋กœ ๋ฏฟ์ง€ ๋ง๊ณ , ๋ฌธ์ œ ์ƒํ™ฉ์œผ๋กœ ๋‹ค์‹œ ์„ค๋ช…ํ•œ๋‹ค. +- ์š”๊ตฌ์‚ฌํ•ญ ๋ฌธ์žฅ์„ ์ •๋ฆฌํ•˜๋Š” ๋ฐ์„œ ๋๋‚ด์ง€ ์•Š๋Š”๋‹ค. +- "๋ฌด์—‡์„ ๋งŒ๋“ค๊นŒ?"๊ฐ€ ์•„๋‹ˆ๋ผ "์ง€๊ธˆ ์–ด๋–ค ๋ฌธ์ œ๊ฐ€ ์žˆ๊ณ , ๊ทธ๊ฑธ ์™œ ํ•ด๊ฒฐํ•˜๋ ค๋Š”๊ฐ€?" ๋กœ ์žฌํ•ด์„ํ•œ๋‹ค. +- ๋‹ค์Œ ๊ด€์ ์„ ๋ถ„๋ฆฌํ•ด์„œ ์ •๋ฆฌํ•œ๋‹ค: + - ์‚ฌ์šฉ์ž ๊ด€์  + - ๋น„์ฆˆ๋‹ˆ์Šค ๊ด€์  + - ์‹œ์Šคํ…œ ๊ด€์  +> ์˜ˆ์‹œ +> "์ฃผ๋ฌธ ์‹คํŒจ ์‹œ ๊ฒฐ์ œ๋ฅผ ์ทจ์†Œํ•œ๋‹ค" โ†’ "๊ฒฐ์ œ ์„ฑ๊ณต/์‹คํŒจ์™€ ์ฃผ๋ฌธ ์ƒํƒœ๊ฐ€ ์–ด๊ธ‹๋‚˜์ง€ ์•Š๋„๋ก ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•˜๋ ค๋Š” ๋ฌธ์ œ" + +### 2๏ธโƒฃ ์• ๋งคํ•œ ์š”๊ตฌ์‚ฌํ•ญ์„ ์ˆจ๊ธฐ์ง€ ๋ง๊ณ  ๋“œ๋Ÿฌ๋‚ธ๋‹ค +- ์ถ”์ธกํ•˜๊ฑฐ๋‚˜ ์•Œ์•„์„œ ๊ฒฐ์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค. +- ์š”๊ตฌ์‚ฌํ•ญ์—์„œ ๊ฒฐ์ •๋˜์ง€ ์•Š์€ ๋ถ€๋ถ„์„ ๋ช…์‹œ์ ์œผ๋กœ ๋‚˜์—ดํ•œ๋‹ค. + **๋‹ค์Œ ์œ ํ˜•์˜ ์งˆ๋ฌธ์„ ๋ฐ˜๋“œ์‹œ ํฌํ•จํ•œ๋‹ค:** +- ์ •์ฑ… ์งˆ๋ฌธ: ๊ธฐ์ค€ ์‹œ์ , ์„ฑ๊ณต/์‹คํŒจ ์กฐ๊ฑด, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ทœ์น™ +- ๊ฒฝ๊ณ„ ์งˆ๋ฌธ: ์–ด๋””๊นŒ์ง€๊ฐ€ ํ•œ ์ฑ…์ž„์ธ๊ฐ€, ์–ด๋””์„œ ๋ถ„๋ฆฌ๋˜๋Š”๊ฐ€ +- ํ™•์žฅ ์งˆ๋ฌธ: ๋‚˜์ค‘์— ๋ฐ”๋€” ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋Š”๊ฐ€ + +### 3๏ธโƒฃ ์š”๊ตฌ์‚ฌํ•ญ ๋ช…ํ™•ํ™”๋ฅผ ์œ„ํ•œ ์งˆ๋ฌธ์„ ๊ฐœ๋ฐœ์ž ๋‹ต๋ณ€์ด ์‰ฌ์šด ํ˜•ํƒœ๋กœ ์ œ์‹œํ•œ๋‹ค +- ์งˆ๋ฌธ์€ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ€์ง„๋‹ค (์ค‘์š”ํ•œ ๊ฒƒ๋ถ€ํ„ฐ). +- ์„ ํƒ์ง€๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ, ์˜ต์…˜ + ์˜ํ–ฅ๋„๋ฅผ ํ•จ๊ป˜ ์ œ์‹œํ•œ๋‹ค. +> ํ˜•์‹ ์˜ˆ์‹œ: +- ์„ ํƒ์ง€ A: ํ•˜๋‚˜์˜ ํŠธ๋žœ์žญ์…˜์œผ๋กœ ์ฒ˜๋ฆฌ โ†’ ๊ตฌํ˜„ ๋‹จ์ˆœ, ํ™•์žฅ์„ฑ ๋‚ฎ์Œ +- ์„ ํƒ์ง€ B: ๋‹จ๊ณ„๋ณ„ ๋ถ„๋ฆฌ โ†’ ๊ตฌ์กฐ ๋ณต์žก, ํ™•์žฅ/๋ณด์ƒ ์ฒ˜๋ฆฌ ์œ ๋ฆฌ + +### 4๏ธโƒฃ ํ•ฉ์˜๋œ ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ๊ฐœ๋… ๋ชจ๋ธ๋ถ€ํ„ฐ ์žก๋Š”๋‹ค +- ๋ฐ”๋กœ ์ฝ”๋“œ๋‚˜ ๊ธฐ์ˆ  ์–˜๊ธฐ๋กœ ๋“ค์–ด๊ฐ€์ง€ ์•Š๋Š”๋‹ค. +- ๋จผ์ € ๋‹ค์Œ์„ ์ •์˜ํ•œ๋‹ค: + - ์•กํ„ฐ (์‚ฌ์šฉ์ž, ์™ธ๋ถ€ ์‹œ์Šคํ…œ) + - ํ•ต์‹ฌ ๋„๋ฉ”์ธ + - ๋ณด์กฐ/์™ธ๋ถ€ ์‹œ์Šคํ…œ +- ์ด ๋‹จ๊ณ„๋Š” โ€œ๊ตฌํ˜„โ€์ด ์•„๋‹ˆ๋ผ ์„ค๊ณ„ ์‚ฌ๊ณ  ์ •๋ ฌ์ด ๋ชฉ์ ์ด๋‹ค. + +### 5๏ธโƒฃ ๋‹ค์ด์–ด๊ทธ๋žจ์€ ํ•ญ์ƒ ์ด์œ  โ†’ ๋‹ค์ด์–ด๊ทธ๋žจ โ†’ ํ•ด์„ ์ˆœ์„œ๋กœ ์ œ์‹œํ•œ๋‹ค +**๋‹ค์ด์–ด๊ทธ๋žจ์„ ๊ทธ๋ฆฌ๊ธฐ ์ „์— ๋ฐ˜๋“œ์‹œ ์„ค๋ช…ํ•œ๋‹ค** +- ์™œ ์ด ๋‹ค์ด์–ด๊ทธ๋žจ์ด ํ•„์š”ํ•œ์ง€ +- ์ด ๋‹ค์ด์–ด๊ทธ๋žจ์œผ๋กœ ๋ฌด์—‡์„ ๊ฒ€์ฆํ•˜๋ ค๋Š”์ง€ + +**๋‹ค์ด์–ด๊ทธ๋žจ์€ Mermaid ๋ฌธ๋ฒ•์œผ๋กœ ์ž‘์„ฑํ•œ๋‹ค** +์‚ฌ์šฉ ๊ธฐ์ค€: +- **์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ** + - ์ฑ…์ž„ ๋ถ„๋ฆฌ + - ํ˜ธ์ถœ ์ˆœ์„œ + - ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ํ™•์ธ +- **ํด๋ž˜์Šค ๋‹ค์ด์–ด๊ทธ๋žจ** + - ๋„๋ฉ”์ธ ์ฑ…์ž„ + - ์˜์กด ๋ฐฉํ–ฅ + - ์‘์ง‘๋„ ํ™•์ธ +- **ERD** + - ์˜์†์„ฑ ๊ตฌ์กฐ + - ๊ด€๊ณ„์˜ ์ฃผ์ธ + - ์ •๊ทœํ™” ์—ฌ๋ถ€ + +### 6๏ธโƒฃ ๋‹ค์ด์–ด๊ทธ๋žจ์„ ๋˜์ง€๊ณ  ๋๋‚ด์ง€ ๋ง๊ณ  ์ฝ๋Š” ๋ฒ•์„ ์งš์–ด์ค€๋‹ค +- "์ด ๊ตฌ์กฐ์—์„œ ํŠนํžˆ ๋ด์•ผ ํ•  ํฌ์ธํŠธ"๋ฅผ 2~3์ค„๋กœ ์„ค๋ช…ํ•œ๋‹ค. +- ์„ค๊ณ„ ์˜๋„๊ฐ€ ๋“œ๋Ÿฌ๋‚˜๋„๋ก ํ•ด์„์„ ๋ถ™์ธ๋‹ค. + +### 7๏ธโƒฃ ์„ค๊ณ„์˜ ์ž ์žฌ ๋ฆฌ์Šคํฌ๋ฅผ ๋ฐ˜๋“œ์‹œ ์–ธ๊ธ‰ํ•œ๋‹ค +- ํ˜„์žฌ ์„ค๊ณ„๊ฐ€ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋Š” ์œ„ํ—˜์„ ์ˆจ๊ธฐ์ง€ ์•Š๋Š”๋‹ค. + - ํŠธ๋žœ์žญ์…˜ ๋น„๋Œ€ํ™” + - ๋„๋ฉ”์ธ ๊ฐ„ ๊ฒฐํ•ฉ๋„ ์ฆ๊ฐ€ + - ์ •์ฑ… ๋ณ€๊ฒฝ ์‹œ ์˜ํ–ฅ ๋ฒ”์œ„ ํ™•๋Œ€ +- ํ•ด๊ฒฐ์ฑ…์€ ์ •๋‹ต์ฒ˜๋Ÿผ ๋งํ•˜์ง€ ์•Š๊ณ  ์„ ํƒ์ง€๋กœ ์ œ์‹œํ•œ๋‹ค. + +### ํ†ค & ์Šคํƒ€์ผ ๊ฐ€์ด๋“œ +- ๊ฐ•์˜์ฒ˜๋Ÿผ ์„ค๋ช…ํ•˜์ง€ ๋ง๊ณ  ์„ค๊ณ„ ๋ฆฌ๋ทฐ ํ†ค์„ ์œ ์ง€ํ•œ๋‹ค +- ์ •๋‹ต์ด๋ผ๊ณ  ์ œ์‹œํ•˜๊ธฐ๋ณด๋‹ค, ๋‹ค๋ฅธ ์„ ํƒ์ง€๊ฐ€ ์žˆ๋‹ค๋ฉด ์ด๋ฅผ ์ œ๊ณตํ•˜๋„๋ก ํ•œ๋‹ค. +- ์ฝ”๋“œ๋ณด๋‹ค ์˜๋„, ์ฑ…์ž„, ๊ฒฝ๊ณ„๋ฅผ ๋” ์ค‘์š”ํ•˜๊ฒŒ ๋‹ค๋ฃฌ๋‹ค +- ๊ตฌํ˜„ ์ „์— ์ƒ๊ฐํ•ด์•ผ ํ•  ๊ฒƒ์„ ๋Œ์–ด๋‚ด๋Š” ๋ฐ ์ง‘์ค‘ํ•œ๋‹ค \ No newline at end of file