From a06b56fa1dc6e066be8baa2584ef99f8af68d9cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Fri, 8 Dec 2023 18:43:30 +0900 Subject: [PATCH 01/16] =?UTF-8?q?feat:=20User=20Entity=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/koreatech/koin/domain/user/User.java | 114 ++++++++++++++++++ .../koin/domain/user/UserGender.java | 7 ++ .../koreatech/koin/domain/user/UserType.java | 16 +++ 3 files changed, 137 insertions(+) create mode 100644 src/main/java/in/koreatech/koin/domain/user/User.java create mode 100644 src/main/java/in/koreatech/koin/domain/user/UserGender.java create mode 100644 src/main/java/in/koreatech/koin/domain/user/UserType.java diff --git a/src/main/java/in/koreatech/koin/domain/user/User.java b/src/main/java/in/koreatech/koin/domain/user/User.java new file mode 100644 index 000000000..77873836e --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/user/User.java @@ -0,0 +1,114 @@ +package in.koreatech.koin.domain.user; + +import in.koreatech.koin.domain.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Lob; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.Instant; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "users") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class User extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + @Lob + @Column(name = "password", nullable = false) + private String password; + + @Size(max = 50) + @Column(name = "nickname", length = 50) + private String nickname; + + @Size(max = 50) + @Column(name = "name", length = 50) + private String name; + + @Size(max = 20) + @Column(name = "phone_number", length = 20) + private String phoneNumber; + + @Size(max = 20) + @NotNull + @Enumerated(EnumType.STRING) + @Column(name = "user_type", nullable = false, length = 20) + private UserType userType; + + @Size(max = 100) + @NotNull + @Column(name = "email", nullable = false, length = 100) + private String email; + + @Column(name = "gender") + @Enumerated(value = EnumType.ORDINAL) + private UserGender gender; + + @NotNull + @Column(name = "is_authed", nullable = false) + private Boolean isAuthed = false; + + @Column(name = "last_logged_at") + private Instant lastLoggedAt; + + @Size(max = 255) + @Column(name = "profile_image_url") + private String profileImageUrl; + + @NotNull + @Column(name = "is_deleted", nullable = false) + private Boolean isDeleted = false; + + @Size(max = 255) + @Column(name = "auth_token") + private String authToken; + + @Size(max = 255) + @Column(name = "auth_expired_at") + private String authExpiredAt; + + @Size(max = 255) + @Column(name = "reset_token") + private String resetToken; + + @Size(max = 255) + @Column(name = "reset_expired_at") + private String resetExpiredAt; + + @Builder + private User(String password, String nickname, String name, String phoneNumber, UserType userType, String email, + UserGender gender, Boolean isAuthed, Instant lastLoggedAt, String profileImageUrl, Boolean isDeleted, + String authToken, String authExpiredAt, String resetToken, String resetExpiredAt) { + this.password = password; + this.nickname = nickname; + this.name = name; + this.phoneNumber = phoneNumber; + this.userType = userType; + this.email = email; + this.gender = gender; + this.isAuthed = isAuthed; + this.lastLoggedAt = lastLoggedAt; + this.profileImageUrl = profileImageUrl; + this.isDeleted = isDeleted; + this.authToken = authToken; + this.authExpiredAt = authExpiredAt; + this.resetToken = resetToken; + this.resetExpiredAt = resetExpiredAt; + } +} diff --git a/src/main/java/in/koreatech/koin/domain/user/UserGender.java b/src/main/java/in/koreatech/koin/domain/user/UserGender.java new file mode 100644 index 000000000..52b8372f0 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/user/UserGender.java @@ -0,0 +1,7 @@ +package in.koreatech.koin.domain.user; + +public enum UserGender { + MAN, + WOMAN, + ; +} diff --git a/src/main/java/in/koreatech/koin/domain/user/UserType.java b/src/main/java/in/koreatech/koin/domain/user/UserType.java new file mode 100644 index 000000000..7f1e80639 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/user/UserType.java @@ -0,0 +1,16 @@ +package in.koreatech.koin.domain.user; + +import lombok.Getter; + +@Getter +public enum UserType { + STUDENT("학생"), + USER("사용자"), + ; + + private final String value; + + UserType(String value) { + this.value = value; + } +} From e75d9793b013729cf69b4b7d621871bbe717aefc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Mon, 11 Dec 2023 16:14:41 +0900 Subject: [PATCH 02/16] =?UTF-8?q?test:=20/user/login=20=EC=9D=B8=EC=88=98?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/in/koreatech/koin/domain/Member.java | 6 +- .../koreatech/koin/domain/user/Student.java | 41 ++++++++++ .../in/koreatech/koin/domain/user/User.java | 1 - .../koin/domain/user/UserIdentity.java | 23 ++++++ .../in/koreatech/koin/dto/LoginResponse.java | 20 +++++ .../koin/dto/TrackSingleResponse.java | 2 +- .../koin/repository/UserRepository.java | 9 +++ .../koin/acceptance/UserApiTest.java | 79 +++++++++++++++++++ 8 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 src/main/java/in/koreatech/koin/domain/user/Student.java create mode 100644 src/main/java/in/koreatech/koin/domain/user/UserIdentity.java create mode 100644 src/main/java/in/koreatech/koin/dto/LoginResponse.java create mode 100644 src/main/java/in/koreatech/koin/repository/UserRepository.java create mode 100644 src/test/java/in/koreatech/koin/acceptance/UserApiTest.java diff --git a/src/main/java/in/koreatech/koin/domain/Member.java b/src/main/java/in/koreatech/koin/domain/Member.java index ee9c76d9b..542fb0cad 100644 --- a/src/main/java/in/koreatech/koin/domain/Member.java +++ b/src/main/java/in/koreatech/koin/domain/Member.java @@ -22,7 +22,7 @@ public class Member extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Integer id; + private Long id; @Size(max = 50) @NotNull @@ -55,8 +55,8 @@ public class Member extends BaseEntity { private Boolean isDeleted = false; @Builder - public Member(String name, String studentNumber, Long trackId, String position, String email, String imageUrl, - Boolean isDeleted) { + private Member(String name, String studentNumber, Long trackId, String position, String email, String imageUrl, + Boolean isDeleted) { this.name = name; this.studentNumber = studentNumber; this.trackId = trackId; diff --git a/src/main/java/in/koreatech/koin/domain/user/Student.java b/src/main/java/in/koreatech/koin/domain/user/Student.java new file mode 100644 index 000000000..19781dd33 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/user/Student.java @@ -0,0 +1,41 @@ +package in.koreatech.koin.domain.user; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.Size; +import lombok.Getter; + +@Getter +@Entity +@Table(name = "students") +public class Student { + + @Id + @OneToOne(orphanRemoval = true) + private User user; + + @Size(max = 255) + @Column(name = "anonymous_nickname") + private String anonymousNickname = "익명_" + System.currentTimeMillis(); + + @Size(max = 20) + @Column(name = "student_number", length = 20) + private String studentNumber; + + + @Size(max = 50) + @Column(name = "major", length = 50) + private String department; + + @Column(name = "identity") + @Enumerated(EnumType.STRING) + private UserIdentity userIdentity; + + @Column(name = "is_graduated") + private Boolean isGraduated; +} diff --git a/src/main/java/in/koreatech/koin/domain/user/User.java b/src/main/java/in/koreatech/koin/domain/user/User.java index 77873836e..345b34698 100644 --- a/src/main/java/in/koreatech/koin/domain/user/User.java +++ b/src/main/java/in/koreatech/koin/domain/user/User.java @@ -45,7 +45,6 @@ public class User extends BaseEntity { @Column(name = "phone_number", length = 20) private String phoneNumber; - @Size(max = 20) @NotNull @Enumerated(EnumType.STRING) @Column(name = "user_type", nullable = false, length = 20) diff --git a/src/main/java/in/koreatech/koin/domain/user/UserIdentity.java b/src/main/java/in/koreatech/koin/domain/user/UserIdentity.java new file mode 100644 index 000000000..b11f349c9 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/user/UserIdentity.java @@ -0,0 +1,23 @@ +package in.koreatech.koin.domain.user; + +import lombok.Getter; + +/** + * 신원 (0: 학생, 1: 대학원생, 2: 교수, 3: 교직원, 4: 졸업생, 5: 점주) + */ +@Getter +public enum UserIdentity { + UNDERGRADUATE("학부생"), + GRADUATE("대학원생"), + PROFESSOR("교수"), + STAFF("교직원"), + ALUMNI("졸업생"), + OWNER("점주"), + ; + + private final String value; + + UserIdentity(String value) { + this.value = value; + } +} diff --git a/src/main/java/in/koreatech/koin/dto/LoginResponse.java b/src/main/java/in/koreatech/koin/dto/LoginResponse.java new file mode 100644 index 000000000..cae140b20 --- /dev/null +++ b/src/main/java/in/koreatech/koin/dto/LoginResponse.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = lombok.AccessLevel.PRIVATE) +@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) +public class LoginResponse { + + private String token; + + @JsonProperty("refresh_token") + private String refreshToken; + @JsonProperty("user_type") + private String userType; +} diff --git a/src/main/java/in/koreatech/koin/dto/TrackSingleResponse.java b/src/main/java/in/koreatech/koin/dto/TrackSingleResponse.java index 61cfa9abf..555886df0 100644 --- a/src/main/java/in/koreatech/koin/dto/TrackSingleResponse.java +++ b/src/main/java/in/koreatech/koin/dto/TrackSingleResponse.java @@ -73,7 +73,7 @@ public static InnerTechStackResponse from(TechStack techStack) { @JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) public static class InnerMemberResponse { - private Integer id; + private Long id; private String name; private String studentNumber; private String position; diff --git a/src/main/java/in/koreatech/koin/repository/UserRepository.java b/src/main/java/in/koreatech/koin/repository/UserRepository.java new file mode 100644 index 000000000..216cce56d --- /dev/null +++ b/src/main/java/in/koreatech/koin/repository/UserRepository.java @@ -0,0 +1,9 @@ +package in.koreatech.koin.repository; + +import in.koreatech.koin.domain.user.User; +import org.springframework.data.repository.Repository; + +public interface UserRepository extends Repository { + + User save(User user); +} diff --git a/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java b/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java new file mode 100644 index 000000000..da43ebc5d --- /dev/null +++ b/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java @@ -0,0 +1,79 @@ +package in.koreatech.koin.acceptance; + + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import static org.springframework.test.annotation.DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD; + +import in.koreatech.koin.domain.user.User; +import in.koreatech.koin.domain.user.UserType; +import in.koreatech.koin.repository.UserRepository; +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.test.annotation.DirtiesContext; + +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) +class UserApiTest { + + @LocalServerPort + int port; + + @Autowired + private UserRepository userRepository; + + @BeforeEach + void setUp() { + RestAssured.port = port; + } + + @Test + @DisplayName("사용자가 로그인을 수행한다") + void userLogin() { + User user = User.builder() + .password("1234") + .nickname("주노") + .name("최준호") + .phoneNumber("010-1234-5678") + .userType(UserType.STUDENT) + .email("test@example.com") + .isAuthed(true) + .isDeleted(false) + .build(); + + userRepository.save(user); + + ExtractableResponse response = RestAssured + .given() + .log().all() + .body(""" + { + "email": "test@example.com", + "password": "1234" + } + """) + .when() + .log().all() + .post("/user/login") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .extract(); + + assertSoftly( + softly -> { + softly.assertThat(response.jsonPath().getString("token")).isNotNull(); + softly.assertThat(response.jsonPath().getString("refresh_token")).isNotNull(); + softly.assertThat(response.jsonPath().getString("user_type")).isEqualTo("STUDENT"); + } + ); + } +} From d3a2db3a62a7e6f67541e2c7def189a7c43aa4da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Mon, 11 Dec 2023 16:46:05 +0900 Subject: [PATCH 03/16] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=BC=88=EB=8C=80=EC=BD=94=EB=93=9C=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koin/controller/UserController.java | 26 ++++++++++++++++ .../in/koreatech/koin/domain/user/User.java | 4 +++ .../koreatech/koin/domain/user/UserType.java | 8 +++-- .../koreatech/koin/dto/UserLoginRequest.java | 15 ++++++++++ ...inResponse.java => UserLoginResponse.java} | 6 +++- .../koin/repository/UserRepository.java | 3 ++ .../koreatech/koin/service/UserService.java | 30 +++++++++++++++++++ 7 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 src/main/java/in/koreatech/koin/controller/UserController.java create mode 100644 src/main/java/in/koreatech/koin/dto/UserLoginRequest.java rename src/main/java/in/koreatech/koin/dto/{LoginResponse.java => UserLoginResponse.java} (73%) create mode 100644 src/main/java/in/koreatech/koin/service/UserService.java diff --git a/src/main/java/in/koreatech/koin/controller/UserController.java b/src/main/java/in/koreatech/koin/controller/UserController.java new file mode 100644 index 000000000..4fc251d22 --- /dev/null +++ b/src/main/java/in/koreatech/koin/controller/UserController.java @@ -0,0 +1,26 @@ +package in.koreatech.koin.controller; + +import in.koreatech.koin.dto.UserLoginRequest; +import in.koreatech.koin.dto.UserLoginResponse; +import in.koreatech.koin.service.UserService; +import jakarta.validation.Valid; +import java.net.URI; +import lombok.RequiredArgsConstructor; +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.RestController; + +@RestController +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @PostMapping("/user/login") + public ResponseEntity login(@RequestBody @Valid UserLoginRequest request) { + UserLoginResponse response = userService.login(request); + return ResponseEntity.created(URI.create("/")) + .body(response); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/user/User.java b/src/main/java/in/koreatech/koin/domain/user/User.java index 345b34698..d57d2c8a3 100644 --- a/src/main/java/in/koreatech/koin/domain/user/User.java +++ b/src/main/java/in/koreatech/koin/domain/user/User.java @@ -110,4 +110,8 @@ private User(String password, String nickname, String name, String phoneNumber, this.resetToken = resetToken; this.resetExpiredAt = resetExpiredAt; } + + public boolean isSamePassword(String password) { + return this.password.equals(password); + } } diff --git a/src/main/java/in/koreatech/koin/domain/user/UserType.java b/src/main/java/in/koreatech/koin/domain/user/UserType.java index 7f1e80639..9eb5abe62 100644 --- a/src/main/java/in/koreatech/koin/domain/user/UserType.java +++ b/src/main/java/in/koreatech/koin/domain/user/UserType.java @@ -4,13 +4,15 @@ @Getter public enum UserType { - STUDENT("학생"), - USER("사용자"), + STUDENT("STUDENT", "학생"), + USER("USER", "사용자"), ; private final String value; + private final String description; - UserType(String value) { + UserType(String value, String description) { this.value = value; + this.description = description; } } diff --git a/src/main/java/in/koreatech/koin/dto/UserLoginRequest.java b/src/main/java/in/koreatech/koin/dto/UserLoginRequest.java new file mode 100644 index 000000000..c2d552955 --- /dev/null +++ b/src/main/java/in/koreatech/koin/dto/UserLoginRequest.java @@ -0,0 +1,15 @@ +package in.koreatech.koin.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + +@Getter +public class UserLoginRequest { + + @Email(message = "이메일 형식을 지켜주세요.") + @NotBlank(message = "이메일을 입력해주세요.") + private String email; + @NotBlank(message = "비밀번호를 입력해주세요.") + private String password; +} diff --git a/src/main/java/in/koreatech/koin/dto/LoginResponse.java b/src/main/java/in/koreatech/koin/dto/UserLoginResponse.java similarity index 73% rename from src/main/java/in/koreatech/koin/dto/LoginResponse.java rename to src/main/java/in/koreatech/koin/dto/UserLoginResponse.java index cae140b20..c25d50b0e 100644 --- a/src/main/java/in/koreatech/koin/dto/LoginResponse.java +++ b/src/main/java/in/koreatech/koin/dto/UserLoginResponse.java @@ -9,7 +9,7 @@ @Getter @AllArgsConstructor(access = lombok.AccessLevel.PRIVATE) @JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) -public class LoginResponse { +public class UserLoginResponse { private String token; @@ -17,4 +17,8 @@ public class LoginResponse { private String refreshToken; @JsonProperty("user_type") private String userType; + + public static UserLoginResponse of(String token, String refreshToken, String userType) { + return new UserLoginResponse(token, refreshToken, userType); + } } diff --git a/src/main/java/in/koreatech/koin/repository/UserRepository.java b/src/main/java/in/koreatech/koin/repository/UserRepository.java index 216cce56d..7931ae276 100644 --- a/src/main/java/in/koreatech/koin/repository/UserRepository.java +++ b/src/main/java/in/koreatech/koin/repository/UserRepository.java @@ -1,9 +1,12 @@ package in.koreatech.koin.repository; import in.koreatech.koin.domain.user.User; +import java.util.Optional; import org.springframework.data.repository.Repository; public interface UserRepository extends Repository { User save(User user); + + Optional findByEmail(String email); } diff --git a/src/main/java/in/koreatech/koin/service/UserService.java b/src/main/java/in/koreatech/koin/service/UserService.java new file mode 100644 index 000000000..fbf982188 --- /dev/null +++ b/src/main/java/in/koreatech/koin/service/UserService.java @@ -0,0 +1,30 @@ +package in.koreatech.koin.service; + +import in.koreatech.koin.domain.user.User; +import in.koreatech.koin.dto.UserLoginRequest; +import in.koreatech.koin.dto.UserLoginResponse; +import in.koreatech.koin.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class UserService { + + private final UserRepository userRepository; + + @Transactional + public UserLoginResponse login(UserLoginRequest request) { + User user = userRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> new IllegalArgumentException("잘못된 로그인 정보입니다.")); + + if (user.isSamePassword(request.getPassword())) { + // TODO: jwt 토큰 발급 + return UserLoginResponse.of("token", "refreshToken", user.getUserType().getValue()); + } else { + throw new IllegalArgumentException("잘못된 로그인 정보입니다."); + } + } +} From d58b1c3f3bd872f1c57565d7c838b2616714975a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Tue, 12 Dec 2023 15:02:28 +0900 Subject: [PATCH 04/16] =?UTF-8?q?feat:=20testcontainer=20=EB=8F=84?= =?UTF-8?q?=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 10 +- .../in/koreatech/koin/auth/JwtProvider.java | 44 +++++++++ .../koreatech/koin/domain/user/Student.java | 3 +- .../koin/repository/UserRepository.java | 2 + .../koreatech/koin/service/UserService.java | 15 ++- src/main/resources/application-example.yml | 23 +++++ src/main/resources/application-test.yml | 24 +++++ .../koin/acceptance/TrackApiTest.java | 15 ++- .../koin/acceptance/UserApiTest.java | 94 +++++++++++-------- 9 files changed, 185 insertions(+), 45 deletions(-) create mode 100644 src/main/java/in/koreatech/koin/auth/JwtProvider.java create mode 100644 src/main/resources/application-test.yml diff --git a/build.gradle b/build.gradle index 7f5da45c9..3b8261804 100644 --- a/build.gradle +++ b/build.gradle @@ -24,10 +24,18 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' - compileOnly 'org.projectlombok:lombok' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + implementation 'com.mysql:mysql-connector-j' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.testcontainers:testcontainers:1.19.3' + testImplementation 'org.testcontainers:junit-jupiter:1.19.3' + testImplementation 'org.testcontainers:mysql' testImplementation 'io.rest-assured:rest-assured:5.3.2' testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/src/main/java/in/koreatech/koin/auth/JwtProvider.java b/src/main/java/in/koreatech/koin/auth/JwtProvider.java new file mode 100644 index 000000000..9f679dfe7 --- /dev/null +++ b/src/main/java/in/koreatech/koin/auth/JwtProvider.java @@ -0,0 +1,44 @@ +package in.koreatech.koin.auth; + +import in.koreatech.koin.domain.user.User; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.security.Key; +import java.time.Instant; +import java.util.Base64; +import java.util.Date; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class JwtProvider { + + @Value("${jwt.secret-key}") + private String secretKey; + + @Value("${jwt.access-token.expiration-time}") + private Long expirationTime; + + public String createToken(User user) { + if (user == null) { + throw new IllegalArgumentException("존재하지 않는 사용자입니다."); + } + + Key key = getSecretKey(); + return Jwts.builder() + .signWith(key) + .header() + .add("typ", "JWT") + .add("alg", key.getAlgorithm()) + .and() + .claim("id", user.getId()) + .expiration(new Date(Instant.now().toEpochMilli() + expirationTime)) + .compact(); + } + + private SecretKey getSecretKey() { + String encoded = Base64.getEncoder().encodeToString(secretKey.getBytes()); + return Keys.hmacShaKeyFor(encoded.getBytes()); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/user/Student.java b/src/main/java/in/koreatech/koin/domain/user/Student.java index 19781dd33..aaade944d 100644 --- a/src/main/java/in/koreatech/koin/domain/user/Student.java +++ b/src/main/java/in/koreatech/koin/domain/user/Student.java @@ -5,6 +5,7 @@ import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import jakarta.validation.constraints.Size; @@ -17,6 +18,7 @@ public class Student { @Id @OneToOne(orphanRemoval = true) + @JoinColumn(name = "user_id") private User user; @Size(max = 255) @@ -27,7 +29,6 @@ public class Student { @Column(name = "student_number", length = 20) private String studentNumber; - @Size(max = 50) @Column(name = "major", length = 50) private String department; diff --git a/src/main/java/in/koreatech/koin/repository/UserRepository.java b/src/main/java/in/koreatech/koin/repository/UserRepository.java index 7931ae276..d38ad52af 100644 --- a/src/main/java/in/koreatech/koin/repository/UserRepository.java +++ b/src/main/java/in/koreatech/koin/repository/UserRepository.java @@ -9,4 +9,6 @@ public interface UserRepository extends Repository { User save(User user); Optional findByEmail(String email); + + Optional findById(Long id); } diff --git a/src/main/java/in/koreatech/koin/service/UserService.java b/src/main/java/in/koreatech/koin/service/UserService.java index fbf982188..7210be278 100644 --- a/src/main/java/in/koreatech/koin/service/UserService.java +++ b/src/main/java/in/koreatech/koin/service/UserService.java @@ -1,9 +1,11 @@ package in.koreatech.koin.service; +import in.koreatech.koin.auth.JwtProvider; import in.koreatech.koin.domain.user.User; import in.koreatech.koin.dto.UserLoginRequest; import in.koreatech.koin.dto.UserLoginResponse; import in.koreatech.koin.repository.UserRepository; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,17 +16,22 @@ public class UserService { private final UserRepository userRepository; + private final JwtProvider jwtProvider; @Transactional public UserLoginResponse login(UserLoginRequest request) { User user = userRepository.findByEmail(request.getEmail()) .orElseThrow(() -> new IllegalArgumentException("잘못된 로그인 정보입니다.")); - if (user.isSamePassword(request.getPassword())) { - // TODO: jwt 토큰 발급 - return UserLoginResponse.of("token", "refreshToken", user.getUserType().getValue()); - } else { + if (!user.isSamePassword(request.getPassword())) { throw new IllegalArgumentException("잘못된 로그인 정보입니다."); } + + String accessToken = jwtProvider.createToken(user); + String refreshToken = String.format("%s%d", UUID.randomUUID(), user.getId()); + + // TODO: access, refresh token Redis에 저장 + return UserLoginResponse.of(accessToken, "??", user.getUserType().getValue()); } + } diff --git a/src/main/resources/application-example.yml b/src/main/resources/application-example.yml index 8b1378917..17755e355 100644 --- a/src/main/resources/application-example.yml +++ b/src/main/resources/application-example.yml @@ -1 +1,24 @@ +jwt: + secret-key: example-secret-key + access-token: + expiration-time: 600000 +spring: + jpa: + properties: + hibernate: + show_sql: true + format_sql: true + + data: + redis: + port: 6379 + host: localhost + +logging: + level: + org: + hibernate: + type: + descriptor: + sql: trace diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 000000000..56f0fc463 --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,24 @@ +jwt: + secret-key: test-secret-key + access-token: + expiration-time: 600000 + +spring: + jpa: + properties: + hibernate: + show_sql: true + format_sql: true + + data: + redis: + port: 8888 + host: localhost + +logging: + level: + org: + hibernate: + type: + descriptor: + sql: trace diff --git a/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java b/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java index c11cb9e57..9e4cdd428 100644 --- a/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java @@ -14,6 +14,7 @@ import io.restassured.response.Response; import java.time.format.DateTimeFormatter; import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -22,9 +23,12 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.testcontainers.containers.MySQLContainer; @SpringBootTest(webEnvironment = RANDOM_PORT) @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) +@ActiveProfiles("test") class TrackApiTest { @LocalServerPort @@ -39,9 +43,17 @@ class TrackApiTest { @Autowired private MemberRepository memberRepository; + private MySQLContainer mySQLContainer = new MySQLContainer("mysql:8"); + @BeforeEach void setUp() { RestAssured.port = port; + mySQLContainer.start(); + } + + @AfterEach + void tearDown() { + mySQLContainer.stop(); } @Test @@ -119,7 +131,8 @@ void findTrack() { softly.assertThat(response.body().jsonPath().getString("TrackName")).isEqualTo(track.getName()); softly.assertThat(response.body().jsonPath().getList("Members")).hasSize(1); - softly.assertThat(response.body().jsonPath().getInt("Members[0].id")).isEqualTo(member.getId()); + softly.assertThat(response.body().jsonPath().getInt("Members[0].id")) + .isEqualTo(member.getId().longValue()); softly.assertThat(response.body().jsonPath().getString("Members[0].name")).isEqualTo(member.getName()); softly.assertThat(response.body().jsonPath().getString("Members[0].student_number")) .isEqualTo(member.getStudentNumber()); diff --git a/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java b/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java index da43ebc5d..40d44385f 100644 --- a/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java @@ -8,11 +8,13 @@ import in.koreatech.koin.domain.user.User; import in.koreatech.koin.domain.user.UserType; import in.koreatech.koin.repository.UserRepository; +//import in.koreatech.koin.repository.UserTokenRepository; import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -30,50 +32,66 @@ class UserApiTest { @Autowired private UserRepository userRepository; +// @Autowired +// private UserTokenRepository tokenRepository; + + private + @BeforeEach void setUp() { RestAssured.port = port; } - @Test + @Nested @DisplayName("사용자가 로그인을 수행한다") - void userLogin() { - User user = User.builder() - .password("1234") - .nickname("주노") - .name("최준호") - .phoneNumber("010-1234-5678") - .userType(UserType.STUDENT) - .email("test@example.com") - .isAuthed(true) - .isDeleted(false) - .build(); - - userRepository.save(user); - - ExtractableResponse response = RestAssured - .given() - .log().all() - .body(""" - { - "email": "test@example.com", - "password": "1234" + class userLogin { + + @Test + @DisplayName("사용자가 로그인을 수행한다") + void userLoginSuccess() { + User user = User.builder() + .password("1234") + .nickname("주노") + .name("최준호") + .phoneNumber("010-1234-5678") + .userType(UserType.STUDENT) + .email("test@example.com") + .isAuthed(true) + .isDeleted(false) + .build(); + + userRepository.save(user); + + ExtractableResponse response = RestAssured + .given() + .log().all() + .body(""" + { + "email": "test@example.com", + "password": "1234" + } + """) + .when() + .log().all() + .post("/user/login") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .extract(); + + User userResult = userRepository.findById(user.getId()).get(); + + assertSoftly( + softly -> { + softly.assertThat(response.jsonPath().getString("token")).isNotNull(); + softly.assertThat(response.jsonPath().getString("refresh_token")).isNotNull(); + softly.assertThat(response.jsonPath().getString("user_type")).isEqualTo("STUDENT"); + softly.assertThat(userResult.getLastLoggedAt()).isNotNull(); } - """) - .when() - .log().all() - .post("/user/login") - .then() - .log().all() - .statusCode(HttpStatus.OK.value()) - .extract(); - - assertSoftly( - softly -> { - softly.assertThat(response.jsonPath().getString("token")).isNotNull(); - softly.assertThat(response.jsonPath().getString("refresh_token")).isNotNull(); - softly.assertThat(response.jsonPath().getString("user_type")).isEqualTo("STUDENT"); - } - ); + ); + } + } + + } From fd7c1d353b43c87a6c82988e67e9acd2916cc9bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Tue, 12 Dec 2023 17:56:44 +0900 Subject: [PATCH 05/16] =?UTF-8?q?test:=20TestContainer=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++ src/main/resources/application-test.yml | 4 ++ .../in/koreatech/koin/AcceptanceTest.java | 55 +++++++++++++++++ .../koin/acceptance/TrackApiTest.java | 23 ++----- .../koreatech/koin/support/DBInitializer.java | 61 +++++++++++++++++++ 5 files changed, 128 insertions(+), 19 deletions(-) create mode 100644 src/main/resources/application-test.yml create mode 100644 src/test/java/in/koreatech/koin/AcceptanceTest.java create mode 100644 src/test/java/in/koreatech/koin/support/DBInitializer.java diff --git a/build.gradle b/build.gradle index 7f5da45c9..2002a34e4 100644 --- a/build.gradle +++ b/build.gradle @@ -24,10 +24,14 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'com.mysql:mysql-connector-j' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.testcontainers:testcontainers:1.19.3' + testImplementation 'org.testcontainers:junit-jupiter:1.19.3' + testImplementation 'org.testcontainers:mysql' testImplementation 'io.rest-assured:rest-assured:5.3.2' testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 000000000..7212038ca --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,4 @@ +spring: + jpa: + hibernate: + ddl-auto: create diff --git a/src/test/java/in/koreatech/koin/AcceptanceTest.java b/src/test/java/in/koreatech/koin/AcceptanceTest.java new file mode 100644 index 000000000..9c8ca5f57 --- /dev/null +++ b/src/test/java/in/koreatech/koin/AcceptanceTest.java @@ -0,0 +1,55 @@ +package in.koreatech.koin; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import in.koreatech.koin.support.DBInitializer; +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; + +@SpringBootTest(webEnvironment = RANDOM_PORT) +@Import(DBInitializer.class) +@ActiveProfiles("test") +public abstract class AcceptanceTest { + + private static final String ROOT = "test"; + private static final String ROOT_PASSWORD = "1234"; + + @LocalServerPort + protected int port; + + @Autowired + private DBInitializer dataInitializer; + + @Container + protected static MySQLContainer container; + + @DynamicPropertySource + private static void configureProperties(final DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", container::getJdbcUrl); + registry.add("spring.datasource.username", () -> ROOT); + registry.add("spring.datasource.password", () -> ROOT_PASSWORD); + } + + static { + container = new MySQLContainer("mysql:8") + .withDatabaseName("test") + .withUsername(ROOT) + .withPassword(ROOT_PASSWORD); + container.start(); + } + + @BeforeEach + void delete() { + dataInitializer.clear(); + RestAssured.port = port; + } +} diff --git a/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java b/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java index c11cb9e57..afb96932f 100644 --- a/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java @@ -1,8 +1,6 @@ package in.koreatech.koin.acceptance; -import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; -import static org.springframework.test.annotation.DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD; - +import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.domain.Member; import in.koreatech.koin.domain.TechStack; import in.koreatech.koin.domain.Track; @@ -14,21 +12,12 @@ import io.restassured.response.Response; import java.time.format.DateTimeFormatter; import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; -import org.springframework.test.annotation.DirtiesContext; - -@SpringBootTest(webEnvironment = RANDOM_PORT) -@DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) -class TrackApiTest { - @LocalServerPort - int port; +class TrackApiTest extends AcceptanceTest { @Autowired private TrackRepository trackRepository; @@ -39,11 +28,6 @@ class TrackApiTest { @Autowired private MemberRepository memberRepository; - @BeforeEach - void setUp() { - RestAssured.port = port; - } - @Test @DisplayName("BCSDLab 트랙 정보를 조회한다") void findTracks() { @@ -119,7 +103,8 @@ void findTrack() { softly.assertThat(response.body().jsonPath().getString("TrackName")).isEqualTo(track.getName()); softly.assertThat(response.body().jsonPath().getList("Members")).hasSize(1); - softly.assertThat(response.body().jsonPath().getInt("Members[0].id")).isEqualTo(member.getId()); + softly.assertThat(response.body().jsonPath().getInt("Members[0].id")) + .isEqualTo(member.getId().longValue()); softly.assertThat(response.body().jsonPath().getString("Members[0].name")).isEqualTo(member.getName()); softly.assertThat(response.body().jsonPath().getString("Members[0].student_number")) .isEqualTo(member.getStudentNumber()); diff --git a/src/test/java/in/koreatech/koin/support/DBInitializer.java b/src/test/java/in/koreatech/koin/support/DBInitializer.java new file mode 100644 index 000000000..dfcfd2b28 --- /dev/null +++ b/src/test/java/in/koreatech/koin/support/DBInitializer.java @@ -0,0 +1,61 @@ +package in.koreatech.koin.support; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; +import javax.sql.DataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.TestComponent; +import org.springframework.transaction.annotation.Transactional; + +@TestComponent +public class DBInitializer { + + private static final int OFF = 0; + private static final int ON = 1; + private static final int COLUMN_INDEX = 1; + + private final List tableNames = new ArrayList<>(); + + @Autowired + private DataSource dataSource; + + @PersistenceContext + private EntityManager entityManager; + + private void findDatabaseTableNames() { + try (final Statement statement = dataSource.getConnection().createStatement()) { + ResultSet resultSet = statement.executeQuery("SHOW TABLES"); + while (resultSet.next()) { + final String tableName = resultSet.getString(COLUMN_INDEX); + tableNames.add(tableName); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void truncate() { + setForeignKeyCheck(OFF); + for (String tableName : tableNames) { + entityManager.createNativeQuery(String.format("TRUNCATE TABLE %s", tableName)).executeUpdate(); + } + setForeignKeyCheck(ON); + } + + private void setForeignKeyCheck(int mode) { + entityManager.createNativeQuery(String.format("SET FOREIGN_KEY_CHECKS = %d", mode)).executeUpdate(); + } + + @Transactional + public void clear() { + if (tableNames.isEmpty()) { + findDatabaseTableNames(); + } + entityManager.clear(); + truncate(); + } +} From d2357b7c7d5db7d433d517c4f0cc2f136f599fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Sat, 16 Dec 2023 14:12:51 +0900 Subject: [PATCH 06/16] =?UTF-8?q?test:=20TestContainer=20MySQL=20=EB=B2=84?= =?UTF-8?q?=EC=A0=84=205.7=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/in/koreatech/koin/AcceptanceTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/java/in/koreatech/koin/AcceptanceTest.java b/src/test/java/in/koreatech/koin/AcceptanceTest.java index 9c8ca5f57..15c5cbba9 100644 --- a/src/test/java/in/koreatech/koin/AcceptanceTest.java +++ b/src/test/java/in/koreatech/koin/AcceptanceTest.java @@ -40,10 +40,11 @@ private static void configureProperties(final DynamicPropertyRegistry registry) } static { - container = new MySQLContainer("mysql:8") + container = (MySQLContainer) new MySQLContainer("mysql:5.7.34") .withDatabaseName("test") .withUsername(ROOT) - .withPassword(ROOT_PASSWORD); + .withPassword(ROOT_PASSWORD) + .withCommand("--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"); container.start(); } From 9dfd94833ab2c2db92c31db9877ae39b3960848b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Sat, 16 Dec 2023 14:29:01 +0900 Subject: [PATCH 07/16] =?UTF-8?q?test:=20RestAssured=20port=20=EC=84=A0?= =?UTF-8?q?=EC=96=B8=EC=A1=B0=EA=B1=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/in/koreatech/koin/AcceptanceTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/in/koreatech/koin/AcceptanceTest.java b/src/test/java/in/koreatech/koin/AcceptanceTest.java index 15c5cbba9..e57d21854 100644 --- a/src/test/java/in/koreatech/koin/AcceptanceTest.java +++ b/src/test/java/in/koreatech/koin/AcceptanceTest.java @@ -50,7 +50,9 @@ private static void configureProperties(final DynamicPropertyRegistry registry) @BeforeEach void delete() { + if (RestAssured.port == RestAssured.UNDEFINED_PORT) { + RestAssured.port = port; + } dataInitializer.clear(); - RestAssured.port = port; } } From 61e703ad6966c5195808c38ef963b0d209c79952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Sat, 16 Dec 2023 16:47:08 +0900 Subject: [PATCH 08/16] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../koreatech/koin/domain/user/Student.java | 6 +- .../in/koreatech/koin/domain/user/User.java | 12 +- .../koreatech/koin/domain/user/UserToken.java | 31 +++++ .../koreatech/koin/dto/UserLoginRequest.java | 1 + .../koin/repository/UserTokenRepository.java | 12 ++ .../koreatech/koin/service/UserService.java | 10 +- src/main/resources/application-example.yml | 2 +- src/main/resources/application-test.yml | 8 +- .../in/koreatech/koin/AcceptanceTest.java | 21 ++- .../koin/acceptance/UserApiTest.java | 123 ++++++++---------- 10 files changed, 126 insertions(+), 100 deletions(-) create mode 100644 src/main/java/in/koreatech/koin/domain/user/UserToken.java create mode 100644 src/main/java/in/koreatech/koin/repository/UserTokenRepository.java diff --git a/src/main/java/in/koreatech/koin/domain/user/Student.java b/src/main/java/in/koreatech/koin/domain/user/Student.java index aaade944d..a64029c93 100644 --- a/src/main/java/in/koreatech/koin/domain/user/Student.java +++ b/src/main/java/in/koreatech/koin/domain/user/Student.java @@ -5,8 +5,6 @@ import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import jakarta.validation.constraints.Size; import lombok.Getter; @@ -17,9 +15,7 @@ public class Student { @Id - @OneToOne(orphanRemoval = true) - @JoinColumn(name = "user_id") - private User user; + private Long userId; @Size(max = 255) @Column(name = "anonymous_nickname") diff --git a/src/main/java/in/koreatech/koin/domain/user/User.java b/src/main/java/in/koreatech/koin/domain/user/User.java index d57d2c8a3..b2cc3276e 100644 --- a/src/main/java/in/koreatech/koin/domain/user/User.java +++ b/src/main/java/in/koreatech/koin/domain/user/User.java @@ -12,7 +12,7 @@ import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; -import java.time.Instant; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -64,7 +64,7 @@ public class User extends BaseEntity { private Boolean isAuthed = false; @Column(name = "last_logged_at") - private Instant lastLoggedAt; + private LocalDateTime lastLoggedAt; @Size(max = 255) @Column(name = "profile_image_url") @@ -91,9 +91,11 @@ public class User extends BaseEntity { private String resetExpiredAt; @Builder - private User(String password, String nickname, String name, String phoneNumber, UserType userType, String email, - UserGender gender, Boolean isAuthed, Instant lastLoggedAt, String profileImageUrl, Boolean isDeleted, - String authToken, String authExpiredAt, String resetToken, String resetExpiredAt) { + public User(String password, String nickname, String name, String phoneNumber, UserType userType, + String email, + UserGender gender, Boolean isAuthed, LocalDateTime lastLoggedAt, String profileImageUrl, + Boolean isDeleted, + String authToken, String authExpiredAt, String resetToken, String resetExpiredAt) { this.password = password; this.nickname = nickname; this.name = name; diff --git a/src/main/java/in/koreatech/koin/domain/user/UserToken.java b/src/main/java/in/koreatech/koin/domain/user/UserToken.java new file mode 100644 index 000000000..b7b23c6b3 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/user/UserToken.java @@ -0,0 +1,31 @@ +package in.koreatech.koin.domain.user; + +import java.util.concurrent.TimeUnit; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.TimeToLive; + +@Getter +@RedisHash("refreshToken") +public class UserToken { + + @Id + private Long id; + + private final String refreshToken; + + @TimeToLive(unit = TimeUnit.DAYS) + private final Long expiration; + + private UserToken(Long id, String refreshToken, Long expiration) { + this.id = id; + this.refreshToken = refreshToken; + this.expiration = expiration; + + } + + public static UserToken create(Long userId, String refreshToken) { + return new UserToken(userId, refreshToken, 3L); + } +} diff --git a/src/main/java/in/koreatech/koin/dto/UserLoginRequest.java b/src/main/java/in/koreatech/koin/dto/UserLoginRequest.java index c2d552955..25ea66541 100644 --- a/src/main/java/in/koreatech/koin/dto/UserLoginRequest.java +++ b/src/main/java/in/koreatech/koin/dto/UserLoginRequest.java @@ -10,6 +10,7 @@ public class UserLoginRequest { @Email(message = "이메일 형식을 지켜주세요.") @NotBlank(message = "이메일을 입력해주세요.") private String email; + @NotBlank(message = "비밀번호를 입력해주세요.") private String password; } diff --git a/src/main/java/in/koreatech/koin/repository/UserTokenRepository.java b/src/main/java/in/koreatech/koin/repository/UserTokenRepository.java new file mode 100644 index 000000000..dff4366e9 --- /dev/null +++ b/src/main/java/in/koreatech/koin/repository/UserTokenRepository.java @@ -0,0 +1,12 @@ +package in.koreatech.koin.repository; + +import in.koreatech.koin.domain.user.UserToken; +import java.util.Optional; +import org.springframework.data.repository.Repository; + +public interface UserTokenRepository extends Repository { + + UserToken save(UserToken userToken); + + Optional findById(Long userId); +} diff --git a/src/main/java/in/koreatech/koin/service/UserService.java b/src/main/java/in/koreatech/koin/service/UserService.java index 7210be278..89a4cd762 100644 --- a/src/main/java/in/koreatech/koin/service/UserService.java +++ b/src/main/java/in/koreatech/koin/service/UserService.java @@ -2,9 +2,11 @@ import in.koreatech.koin.auth.JwtProvider; import in.koreatech.koin.domain.user.User; +import in.koreatech.koin.domain.user.UserToken; import in.koreatech.koin.dto.UserLoginRequest; import in.koreatech.koin.dto.UserLoginResponse; import in.koreatech.koin.repository.UserRepository; +import in.koreatech.koin.repository.UserTokenRepository; import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -15,8 +17,9 @@ @Transactional(readOnly = true) public class UserService { - private final UserRepository userRepository; private final JwtProvider jwtProvider; + private final UserRepository userRepository; + private final UserTokenRepository userTokenRepository; @Transactional public UserLoginResponse login(UserLoginRequest request) { @@ -29,9 +32,8 @@ public UserLoginResponse login(UserLoginRequest request) { String accessToken = jwtProvider.createToken(user); String refreshToken = String.format("%s%d", UUID.randomUUID(), user.getId()); + UserToken savedToken = userTokenRepository.save(UserToken.create(user.getId(), refreshToken)); - // TODO: access, refresh token Redis에 저장 - return UserLoginResponse.of(accessToken, "??", user.getUserType().getValue()); + return UserLoginResponse.of(accessToken, savedToken.getRefreshToken(), user.getUserType().getValue()); } - } diff --git a/src/main/resources/application-example.yml b/src/main/resources/application-example.yml index 17755e355..89fc6a9b4 100644 --- a/src/main/resources/application-example.yml +++ b/src/main/resources/application-example.yml @@ -1,5 +1,5 @@ jwt: - secret-key: example-secret-key + secret-key: EXAMPLE7A3E4F37B3DAD9CD8KEY6AA4B1AF7123!@# access-token: expiration-time: 600000 diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 26aac25f2..94a99b8c5 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -1,5 +1,5 @@ jwt: - secret-key: test-secret-key + secret-key: EXAMPLE7A3E4F37B3DAD9CD8KEY6AA4B1AF7123!@# access-token: expiration-time: 600000 @@ -12,12 +12,6 @@ spring: hibernate: ddl-auto: create - - data: - redis: - port: 8888 - host: localhost - logging: level: org: diff --git a/src/test/java/in/koreatech/koin/AcceptanceTest.java b/src/test/java/in/koreatech/koin/AcceptanceTest.java index e57d21854..c2a8b6615 100644 --- a/src/test/java/in/koreatech/koin/AcceptanceTest.java +++ b/src/test/java/in/koreatech/koin/AcceptanceTest.java @@ -12,8 +12,9 @@ import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.utility.DockerImageName; @SpringBootTest(webEnvironment = RANDOM_PORT) @Import(DBInitializer.class) @@ -29,23 +30,31 @@ public abstract class AcceptanceTest { @Autowired private DBInitializer dataInitializer; - @Container - protected static MySQLContainer container; + protected static MySQLContainer mySqlContainer; + protected static GenericContainer redisContainer; @DynamicPropertySource private static void configureProperties(final DynamicPropertyRegistry registry) { - registry.add("spring.datasource.url", container::getJdbcUrl); + registry.add("spring.datasource.url", mySqlContainer::getJdbcUrl); registry.add("spring.datasource.username", () -> ROOT); registry.add("spring.datasource.password", () -> ROOT_PASSWORD); + registry.add("spring.data.redis.host", redisContainer::getHost); + registry.add("spring.data.redis.port", () -> redisContainer.getMappedPort(6379).toString()); } static { - container = (MySQLContainer) new MySQLContainer("mysql:5.7.34") + mySqlContainer = (MySQLContainer) new MySQLContainer("mysql:5.7.34") .withDatabaseName("test") .withUsername(ROOT) .withPassword(ROOT_PASSWORD) .withCommand("--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"); - container.start(); + + redisContainer = new GenericContainer<>( + DockerImageName.parse("redis:4.0.10")) + .withExposedPorts(6379); + + mySqlContainer.start(); + redisContainer.start(); } @BeforeEach diff --git a/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java b/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java index 40d44385f..788996a22 100644 --- a/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java @@ -2,96 +2,75 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; -import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; -import static org.springframework.test.annotation.DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD; +import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.domain.user.User; +import in.koreatech.koin.domain.user.UserToken; import in.koreatech.koin.domain.user.UserType; import in.koreatech.koin.repository.UserRepository; -//import in.koreatech.koin.repository.UserTokenRepository; +import in.koreatech.koin.repository.UserTokenRepository; import io.restassured.RestAssured; +import io.restassured.http.ContentType; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; -import org.springframework.test.annotation.DirtiesContext; -@SpringBootTest(webEnvironment = RANDOM_PORT) -@DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) -class UserApiTest { - - @LocalServerPort - int port; +class UserApiTest extends AcceptanceTest { @Autowired private UserRepository userRepository; -// @Autowired -// private UserTokenRepository tokenRepository; - - private - - @BeforeEach - void setUp() { - RestAssured.port = port; - } + @Autowired + private UserTokenRepository tokenRepository; - @Nested + @Test @DisplayName("사용자가 로그인을 수행한다") - class userLogin { - - @Test - @DisplayName("사용자가 로그인을 수행한다") - void userLoginSuccess() { - User user = User.builder() - .password("1234") - .nickname("주노") - .name("최준호") - .phoneNumber("010-1234-5678") - .userType(UserType.STUDENT) - .email("test@example.com") - .isAuthed(true) - .isDeleted(false) - .build(); - - userRepository.save(user); - - ExtractableResponse response = RestAssured - .given() - .log().all() - .body(""" - { - "email": "test@example.com", - "password": "1234" - } - """) - .when() - .log().all() - .post("/user/login") - .then() - .log().all() - .statusCode(HttpStatus.OK.value()) - .extract(); - - User userResult = userRepository.findById(user.getId()).get(); - - assertSoftly( - softly -> { - softly.assertThat(response.jsonPath().getString("token")).isNotNull(); - softly.assertThat(response.jsonPath().getString("refresh_token")).isNotNull(); - softly.assertThat(response.jsonPath().getString("user_type")).isEqualTo("STUDENT"); - softly.assertThat(userResult.getLastLoggedAt()).isNotNull(); + void userLoginSuccess() { + User user = User.builder() + .password("1234") + .nickname("주노") + .name("최준호") + .phoneNumber("010-1234-5678") + .userType(UserType.STUDENT) + .email("test@example.com") + .isAuthed(true) + .isDeleted(false) + .build(); + + userRepository.save(user); + + ExtractableResponse response = RestAssured + .given() + .log().all() + .body(""" + { + "email": "test@example.com", + "password": "1234" } - ); - } - + """) + .contentType(ContentType.JSON) + .when() + .log().all() + .post("/user/login") + .then() + .log().all() + .statusCode(HttpStatus.CREATED.value()) + .extract(); + + User userResult = userRepository.findById(user.getId()).get(); + UserToken token = tokenRepository.findById(user.getId()).get(); + + assertSoftly( + softly -> { + softly.assertThat(response.jsonPath().getString("token")).isNotNull(); + softly.assertThat(response.jsonPath().getString("refresh_token")).isNotNull(); + softly.assertThat(response.jsonPath().getString("refresh_token")) + .isEqualTo(token.getRefreshToken()); + softly.assertThat(response.jsonPath().getString("user_type")).isEqualTo("STUDENT"); + } + ); } - - } From 7e2acdd0b8de0667cbbef4fa08c639888471cb61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Sat, 16 Dec 2023 16:53:29 +0900 Subject: [PATCH 09/16] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EB=A7=88=EC=A7=80=EB=A7=89=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EB=82=A0=EC=A7=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/in/koreatech/koin/domain/user/User.java | 4 ++++ src/main/java/in/koreatech/koin/service/UserService.java | 5 ++++- src/test/java/in/koreatech/koin/acceptance/UserApiTest.java | 3 ++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/user/User.java b/src/main/java/in/koreatech/koin/domain/user/User.java index b2cc3276e..d7ce56cdf 100644 --- a/src/main/java/in/koreatech/koin/domain/user/User.java +++ b/src/main/java/in/koreatech/koin/domain/user/User.java @@ -116,4 +116,8 @@ public User(String password, String nickname, String name, String phoneNumber, U public boolean isSamePassword(String password) { return this.password.equals(password); } + + public void updateLastLoggedTime(LocalDateTime lastLoggedTime) { + lastLoggedAt = lastLoggedTime; + } } diff --git a/src/main/java/in/koreatech/koin/service/UserService.java b/src/main/java/in/koreatech/koin/service/UserService.java index 89a4cd762..1244793ad 100644 --- a/src/main/java/in/koreatech/koin/service/UserService.java +++ b/src/main/java/in/koreatech/koin/service/UserService.java @@ -7,6 +7,7 @@ import in.koreatech.koin.dto.UserLoginResponse; import in.koreatech.koin.repository.UserRepository; import in.koreatech.koin.repository.UserTokenRepository; +import java.time.LocalDateTime; import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -33,7 +34,9 @@ public UserLoginResponse login(UserLoginRequest request) { String accessToken = jwtProvider.createToken(user); String refreshToken = String.format("%s%d", UUID.randomUUID(), user.getId()); UserToken savedToken = userTokenRepository.save(UserToken.create(user.getId(), refreshToken)); + user.updateLastLoggedTime(LocalDateTime.now()); + User saved = userRepository.save(user); - return UserLoginResponse.of(accessToken, savedToken.getRefreshToken(), user.getUserType().getValue()); + return UserLoginResponse.of(accessToken, savedToken.getRefreshToken(), saved.getUserType().getValue()); } } diff --git a/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java b/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java index 788996a22..b66d4efff 100644 --- a/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java @@ -61,7 +61,7 @@ void userLoginSuccess() { .extract(); User userResult = userRepository.findById(user.getId()).get(); - UserToken token = tokenRepository.findById(user.getId()).get(); + UserToken token = tokenRepository.findById(userResult.getId()).get(); assertSoftly( softly -> { @@ -70,6 +70,7 @@ void userLoginSuccess() { softly.assertThat(response.jsonPath().getString("refresh_token")) .isEqualTo(token.getRefreshToken()); softly.assertThat(response.jsonPath().getString("user_type")).isEqualTo("STUDENT"); + softly.assertThat(userResult.getLastLoggedAt()).isNotNull(); } ); } From ece89a80ef282ba190e03a6d344fad6d8f7af5df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Sat, 16 Dec 2023 17:43:21 +0900 Subject: [PATCH 10/16] =?UTF-8?q?test:=20ActiveProfiles=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/in/koreatech/koin/KoinApplicationTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/in/koreatech/koin/KoinApplicationTest.java b/src/test/java/in/koreatech/koin/KoinApplicationTest.java index e49beb282..c4a59d21a 100644 --- a/src/test/java/in/koreatech/koin/KoinApplicationTest.java +++ b/src/test/java/in/koreatech/koin/KoinApplicationTest.java @@ -2,8 +2,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; @SpringBootTest +@ActiveProfiles("test") class KoinApplicationTest { @Test From d5ef177aef35b8a0da82c61ffcd67bff08ffec41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Tue, 19 Dec 2023 13:57:51 +0900 Subject: [PATCH 11/16] =?UTF-8?q?build:=20=ED=86=A0=ED=81=B0=20=EA=B0=92?= =?UTF-8?q?=20minute=20=EC=A3=BC=EC=84=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 94a99b8c5..02fe552fa 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -1,7 +1,7 @@ jwt: secret-key: EXAMPLE7A3E4F37B3DAD9CD8KEY6AA4B1AF7123!@# access-token: - expiration-time: 600000 + expiration-time: 600000 # (ms) = 10 minutes spring: jpa: From 4c4f357576980d85a8f4fee4676c8d378ca5009b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Tue, 19 Dec 2023 13:58:26 +0900 Subject: [PATCH 12/16] =?UTF-8?q?refactor:=20refreshToken=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=EC=9D=BC=EC=9E=90=2014=EC=9D=BC=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/in/koreatech/koin/domain/user/UserToken.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/in/koreatech/koin/domain/user/UserToken.java b/src/main/java/in/koreatech/koin/domain/user/UserToken.java index b7b23c6b3..efe335cae 100644 --- a/src/main/java/in/koreatech/koin/domain/user/UserToken.java +++ b/src/main/java/in/koreatech/koin/domain/user/UserToken.java @@ -10,6 +10,8 @@ @RedisHash("refreshToken") public class UserToken { + private static final long REFRESH_TOKEN_EXPIRE_DAY = 14L; + @Id private Long id; @@ -22,10 +24,9 @@ private UserToken(Long id, String refreshToken, Long expiration) { this.id = id; this.refreshToken = refreshToken; this.expiration = expiration; - } public static UserToken create(Long userId, String refreshToken) { - return new UserToken(userId, refreshToken, 3L); + return new UserToken(userId, refreshToken, REFRESH_TOKEN_EXPIRE_DAY); } } From 538aae075e75261cd69aa0655492dfaabe1bc431 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Tue, 19 Dec 2023 13:58:42 +0900 Subject: [PATCH 13/16] =?UTF-8?q?build:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=84=A4=EC=A0=95=ED=8C=8C=EC=9D=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-example.yml | 24 ---------------------- 1 file changed, 24 deletions(-) delete mode 100644 src/main/resources/application-example.yml diff --git a/src/main/resources/application-example.yml b/src/main/resources/application-example.yml deleted file mode 100644 index 89fc6a9b4..000000000 --- a/src/main/resources/application-example.yml +++ /dev/null @@ -1,24 +0,0 @@ -jwt: - secret-key: EXAMPLE7A3E4F37B3DAD9CD8KEY6AA4B1AF7123!@# - access-token: - expiration-time: 600000 - -spring: - jpa: - properties: - hibernate: - show_sql: true - format_sql: true - - data: - redis: - port: 6379 - host: localhost - -logging: - level: - org: - hibernate: - type: - descriptor: - sql: trace From a7e67d102b5006f14f8bad05d479123a84786033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Thu, 21 Dec 2023 16:37:18 +0900 Subject: [PATCH 14/16] =?UTF-8?q?refactor:=20=EC=98=88=EC=99=B8=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=EB=B6=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/GlobalExceptionHandler.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/main/java/in/koreatech/koin/controller/GlobalExceptionHandler.java diff --git a/src/main/java/in/koreatech/koin/controller/GlobalExceptionHandler.java b/src/main/java/in/koreatech/koin/controller/GlobalExceptionHandler.java new file mode 100644 index 000000000..9d3288582 --- /dev/null +++ b/src/main/java/in/koreatech/koin/controller/GlobalExceptionHandler.java @@ -0,0 +1,25 @@ +package in.koreatech.koin.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler + public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + log.warn(e.getMessage()); + return ResponseEntity.badRequest().body(e.getMessage()); + } + + @ExceptionHandler + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException e) { + log.warn(e.getMessage()); + return ResponseEntity.badRequest().body(e.getMessage()); + } +} From f27f9cae1af0f683f1eabe717215b25dd0b24bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Thu, 21 Dec 2023 16:37:34 +0900 Subject: [PATCH 15/16] =?UTF-8?q?test:=20=ED=95=99=EA=B5=90=20=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=EB=A1=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/java/in/koreatech/koin/acceptance/UserApiTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java b/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java index b66d4efff..06c59555d 100644 --- a/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java @@ -35,7 +35,7 @@ void userLoginSuccess() { .name("최준호") .phoneNumber("010-1234-5678") .userType(UserType.STUDENT) - .email("test@example.com") + .email("test@koreatech.ac.kr") .isAuthed(true) .isDeleted(false) .build(); @@ -47,7 +47,7 @@ void userLoginSuccess() { .log().all() .body(""" { - "email": "test@example.com", + "email": "test@koreatech.ac.kr", "password": "1234" } """) From 2e779bc890108628c25e4b0101f8d239be6ce4d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=AC=E1=84=8C=E1=85=AE=E1=86=AB=E1=84=92?= =?UTF-8?q?=E1=85=A9?= Date: Thu, 21 Dec 2023 17:27:52 +0900 Subject: [PATCH 16/16] =?UTF-8?q?refactor:=20=EB=B0=98=ED=99=98=EA=B0=92?= =?UTF-8?q?=20accessToken=EC=9E=84=EC=9D=84=20=EB=AA=85=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/in/koreatech/koin/dto/UserLoginResponse.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/in/koreatech/koin/dto/UserLoginResponse.java b/src/main/java/in/koreatech/koin/dto/UserLoginResponse.java index c25d50b0e..d8e74d5c8 100644 --- a/src/main/java/in/koreatech/koin/dto/UserLoginResponse.java +++ b/src/main/java/in/koreatech/koin/dto/UserLoginResponse.java @@ -1,20 +1,19 @@ package in.koreatech.koin.dto; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.PropertyNamingStrategies; -import com.fasterxml.jackson.databind.annotation.JsonNaming; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor(access = lombok.AccessLevel.PRIVATE) -@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) public class UserLoginResponse { - private String token; + @JsonProperty("token") + private String accessToken; @JsonProperty("refresh_token") private String refreshToken; + @JsonProperty("user_type") private String userType;