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] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EB=A1=9C?= =?UTF-8?q?=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"); + } + ); } - - }