From b70335a3004505aa6e33f58bc42d454aa5e3ce56 Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Sat, 17 May 2025 14:10:52 +0900 Subject: [PATCH 01/21] :sparkles: feat : make HealthCheck api --- .../controller/HealthCheckController.java | 25 ++++++++++ .../runimo/runimo/common/GlobalConsts.java | 3 +- .../runimo/runimo/config/SecurityConfig.java | 2 + .../controller/HealthCheckControllerTest.java | 48 +++++++++++++++++++ 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/runimo/runimo/checker/controller/HealthCheckController.java create mode 100644 src/test/java/org/runimo/runimo/checker/controller/HealthCheckControllerTest.java diff --git a/src/main/java/org/runimo/runimo/checker/controller/HealthCheckController.java b/src/main/java/org/runimo/runimo/checker/controller/HealthCheckController.java new file mode 100644 index 0000000..9172969 --- /dev/null +++ b/src/main/java/org/runimo/runimo/checker/controller/HealthCheckController.java @@ -0,0 +1,25 @@ +package org.runimo.runimo.checker.controller; + +import org.runimo.runimo.common.log.ServiceLog; +import org.runimo.runimo.common.response.SuccessResponse; +import org.runimo.runimo.exceptions.code.ExampleErrorCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/checker") +public class HealthCheckController { + + /** + * health check api + */ + @ServiceLog + @GetMapping("/health-check") + public ResponseEntity> healthCheck() { + return ResponseEntity.ok( + SuccessResponse.of(ExampleErrorCode.SUCCESS, "Health check success !")); + } + +} diff --git a/src/main/java/org/runimo/runimo/common/GlobalConsts.java b/src/main/java/org/runimo/runimo/common/GlobalConsts.java index 7d9fcfb..3b860d0 100644 --- a/src/main/java/org/runimo/runimo/common/GlobalConsts.java +++ b/src/main/java/org/runimo/runimo/common/GlobalConsts.java @@ -14,7 +14,8 @@ public final class GlobalConsts { "/api/v1/auth", "/swagger-ui", "/v3/api-docs", - "/actuator" + "/actuator", + "/checker" ); public static final String EMPTYFIELD = "EMPTY"; diff --git a/src/main/java/org/runimo/runimo/config/SecurityConfig.java b/src/main/java/org/runimo/runimo/config/SecurityConfig.java index 2ec43c2..3c28387 100644 --- a/src/main/java/org/runimo/runimo/config/SecurityConfig.java +++ b/src/main/java/org/runimo/runimo/config/SecurityConfig.java @@ -29,6 +29,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti ) .authorizeHttpRequests(authorize -> authorize .requestMatchers("/api/v1/auth/**").permitAll() + .requestMatchers("/checker/**").permitAll() .requestMatchers(("/error")).permitAll() .anyRequest().authenticated() ) @@ -52,6 +53,7 @@ public SecurityFilterChain devSecurityFilterChain(HttpSecurity http) throws Exce .permitAll() .requestMatchers(("/error")).permitAll() .requestMatchers("/api/v1/users/**").hasAnyRole("USER", "ADMIN") + .requestMatchers("/checker/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); diff --git a/src/test/java/org/runimo/runimo/checker/controller/HealthCheckControllerTest.java b/src/test/java/org/runimo/runimo/checker/controller/HealthCheckControllerTest.java new file mode 100644 index 0000000..45f36ba --- /dev/null +++ b/src/test/java/org/runimo/runimo/checker/controller/HealthCheckControllerTest.java @@ -0,0 +1,48 @@ +package org.runimo.runimo.checker.controller; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.*; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.runimo.runimo.CleanUpUtil; +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.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +class HealthCheckControllerTest { + + @LocalServerPort + int port; + + @Autowired + private CleanUpUtil cleanUpUtil; + + @BeforeEach + void setUp() { + RestAssured.port = port; + } + + + @Test + void healthCheck() { + + // when & then + given() + .contentType(ContentType.JSON) + .when() + .get("/checker/health-check") + .then() + .log().all() + .statusCode(200) + .body("payload", equalTo("Health check success !")); + } +} \ No newline at end of file From 29f06c5946532129fc9b25165e70e3500fee651b Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Mon, 19 May 2025 01:27:26 +0900 Subject: [PATCH 02/21] :sparkles: feat : make refresh token delete logic in TokenRefreshService --- .../repository/DatabaseTokenRepository.java | 21 ++++++++++++------- .../repository/InMemoryTokenRepository.java | 7 +++++++ .../auth/repository/JwtTokenRepository.java | 2 ++ .../repository/RefreshTokenJpaRepository.java | 2 ++ .../auth/service/TokenRefreshService.java | 9 ++++++++ 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/runimo/runimo/auth/repository/DatabaseTokenRepository.java b/src/main/java/org/runimo/runimo/auth/repository/DatabaseTokenRepository.java index f9f0181..55c3854 100644 --- a/src/main/java/org/runimo/runimo/auth/repository/DatabaseTokenRepository.java +++ b/src/main/java/org/runimo/runimo/auth/repository/DatabaseTokenRepository.java @@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; @Profile({"prod", "dev"}) @Repository @@ -20,9 +21,8 @@ public class DatabaseTokenRepository implements JwtTokenRepository { /** - * @param userId 사용자 ID - * 만료되지않은 refreshToken을 조회합니다. - * */ + * @param userId 사용자 ID 만료되지않은 refreshToken을 조회합니다. + */ @Override public Optional findRefreshTokenByUserId(final Long userId) { LocalDateTime REPLACE_CUTOFF_TIME = LocalDateTime.now() @@ -32,10 +32,9 @@ public Optional findRefreshTokenByUserId(final Long userId) { } /** - * @param userId 사용자 ID - * @param refreshToken refreshToken - * refreshToken 엔티티를 UPSERT합니다. - * */ + * @param userId 사용자 ID + * @param refreshToken refreshToken refreshToken 엔티티를 UPSERT합니다. + */ @Override public void saveRefreshTokenWithUserId(final Long userId, final String refreshToken) { @@ -52,5 +51,13 @@ public void saveRefreshTokenWithUserId(final Long userId, final String refreshTo refreshTokenJpaRepository.save(updatedRefreshToken); } + /** + * @param userId 사용자 ID 사용자의 refreshToken을 DELETE 합니다. + */ + @Override + @Transactional + public void deleteRefreshTokenByUserId(Long userId) { + refreshTokenJpaRepository.deleteByUserId(userId); + } } diff --git a/src/main/java/org/runimo/runimo/auth/repository/InMemoryTokenRepository.java b/src/main/java/org/runimo/runimo/auth/repository/InMemoryTokenRepository.java index dce9c94..4b41ea4 100644 --- a/src/main/java/org/runimo/runimo/auth/repository/InMemoryTokenRepository.java +++ b/src/main/java/org/runimo/runimo/auth/repository/InMemoryTokenRepository.java @@ -3,11 +3,13 @@ import java.time.Duration; import java.util.Optional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.runimo.runimo.common.cache.InMemoryCache; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Repository; +@Slf4j @Profile({"test", "local"}) @Repository @RequiredArgsConstructor @@ -27,4 +29,9 @@ public Optional findRefreshTokenByUserId(Long userId) { public void saveRefreshTokenWithUserId(Long userId, String refreshToken) { refreshTokenCache.put(userId, refreshToken, Duration.ofMillis(refreshTokenExpiry)); } + + @Override + public void deleteRefreshTokenByUserId(Long userId) { + refreshTokenCache.remove(userId); + } } diff --git a/src/main/java/org/runimo/runimo/auth/repository/JwtTokenRepository.java b/src/main/java/org/runimo/runimo/auth/repository/JwtTokenRepository.java index 0ebfc01..935e254 100644 --- a/src/main/java/org/runimo/runimo/auth/repository/JwtTokenRepository.java +++ b/src/main/java/org/runimo/runimo/auth/repository/JwtTokenRepository.java @@ -7,4 +7,6 @@ public interface JwtTokenRepository { Optional findRefreshTokenByUserId(Long userId); void saveRefreshTokenWithUserId(Long userId, String refreshToken); + + void deleteRefreshTokenByUserId(Long userId); } diff --git a/src/main/java/org/runimo/runimo/auth/repository/RefreshTokenJpaRepository.java b/src/main/java/org/runimo/runimo/auth/repository/RefreshTokenJpaRepository.java index e2a52b6..620e5c2 100644 --- a/src/main/java/org/runimo/runimo/auth/repository/RefreshTokenJpaRepository.java +++ b/src/main/java/org/runimo/runimo/auth/repository/RefreshTokenJpaRepository.java @@ -15,4 +15,6 @@ public interface RefreshTokenJpaRepository extends JpaRepository :cutOffDateTime") Optional findByUserIdAfterCutoffTime(Long userId, LocalDateTime cutOffDateTime); + + void deleteByUserId(Long userId); } diff --git a/src/main/java/org/runimo/runimo/auth/service/TokenRefreshService.java b/src/main/java/org/runimo/runimo/auth/service/TokenRefreshService.java index 3cf5c76..a5b74d4 100644 --- a/src/main/java/org/runimo/runimo/auth/service/TokenRefreshService.java +++ b/src/main/java/org/runimo/runimo/auth/service/TokenRefreshService.java @@ -48,4 +48,13 @@ public TokenPair refreshAccessToken(String refreshToken) { String newAccessToken = jwtTokenFactory.generateAccessToken(user); return new TokenPair(newAccessToken, refreshToken); } + + /** + * 해당 사용자의 refresh token 삭제 + * + * @param userId 사용자 식별자 + */ + public void removeRefreshToken(Long userId) { + jwtTokenRepository.deleteRefreshTokenByUserId(userId); + } } From 26908488fde018229e594d057663b51c164d9430 Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Mon, 19 May 2025 01:28:10 +0900 Subject: [PATCH 03/21] :sparkles: feat : add refresh token delete logic at withdraw api --- .../user/controller/LogOutController.java | 37 +++++++++++++++++++ .../user/enums/UserHttpResponseCode.java | 6 ++- .../runimo/user/exception/UserException.java | 19 ++++++++++ .../runimo/user/service/WithdrawService.java | 3 ++ 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/runimo/runimo/user/controller/LogOutController.java create mode 100644 src/main/java/org/runimo/runimo/user/exception/UserException.java diff --git a/src/main/java/org/runimo/runimo/user/controller/LogOutController.java b/src/main/java/org/runimo/runimo/user/controller/LogOutController.java new file mode 100644 index 0000000..54fa13a --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/controller/LogOutController.java @@ -0,0 +1,37 @@ +package org.runimo.runimo.user.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.common.response.SuccessResponse; +import org.runimo.runimo.user.enums.UserHttpResponseCode; +import org.runimo.runimo.user.service.usecases.logout.LogOutUsecase; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "USER-LOG-OUT", description = "로그아웃 API") +@RestController +@RequestMapping("/api/v1/users") +@RequiredArgsConstructor +public class LogOutController { + + private final LogOutUsecase logOutUsecase; + + @Operation(summary = "로그아웃", description = "사용자를 로그아웃 처리합니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그아웃 성공"), + @ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음") + }) + @PostMapping("/log-out") + public ResponseEntity> logOut( + @UserId Long userId + ) { + logOutUsecase.execute(userId); + return ResponseEntity.ok() + .body(SuccessResponse.of(UserHttpResponseCode.LOG_OUT_SUCCESS, null)); + } +} diff --git a/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java b/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java index ddbd6a6..15f545a 100644 --- a/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java +++ b/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java @@ -22,7 +22,11 @@ public enum UserHttpResponseCode implements CustomResponseCode { JWT_TOKEN_BROKEN(HttpStatus.BAD_REQUEST, "JWT 토큰이 손상되었습니다", "JWT 토큰이 손상되었습니다"), TOKEN_REFRESH_FAIL(HttpStatus.FORBIDDEN, "토큰 재발급 실패", "Refresh 토큰이 유효하지 않습니다."), TOKEN_INVALID(HttpStatus.UNAUTHORIZED, "인증 실패", "JWT 토큰 인증 실패"), - REFRESH_EXPIRED(HttpStatus.FORBIDDEN, "리프레시 토큰 만료", "리프레시 토큰 만료"); + REFRESH_EXPIRED(HttpStatus.FORBIDDEN, "리프레시 토큰 만료", "리프레시 토큰 만료"), + TOKEN_DELETE_REFRESH_FAIL(HttpStatus.FORBIDDEN, "토큰 삭제 실패", + "사용자가 유효하지 않습니다. Refresh 토큰 삭제에 실패했습니다"), + LOG_OUT_SUCCESS(HttpStatus.OK, "로그아웃 성공", "로그아웃 성공"), + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없음", "사용자를 찾을 수 없음"); private final HttpStatus code; private final String clientMessage; diff --git a/src/main/java/org/runimo/runimo/user/exception/UserException.java b/src/main/java/org/runimo/runimo/user/exception/UserException.java new file mode 100644 index 0000000..1c1a6e1 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/exception/UserException.java @@ -0,0 +1,19 @@ +package org.runimo.runimo.user.exception; + +import org.runimo.runimo.exceptions.BusinessException; +import org.runimo.runimo.exceptions.code.CustomResponseCode; + +public class UserException extends BusinessException { + + protected UserException(CustomResponseCode errorCode) { + super(errorCode); + } + + public UserException(CustomResponseCode errorCode, String logMessage) { + super(errorCode, logMessage); + } + + public static UserException of(CustomResponseCode errorCode) { + return new UserException(errorCode, errorCode.getLogMessage()); + } +} diff --git a/src/main/java/org/runimo/runimo/user/service/WithdrawService.java b/src/main/java/org/runimo/runimo/user/service/WithdrawService.java index a49f63a..ec0272a 100644 --- a/src/main/java/org/runimo/runimo/user/service/WithdrawService.java +++ b/src/main/java/org/runimo/runimo/user/service/WithdrawService.java @@ -3,6 +3,7 @@ import java.util.NoSuchElementException; import lombok.RequiredArgsConstructor; import org.runimo.runimo.auth.service.EncryptUtil; +import org.runimo.runimo.auth.service.TokenRefreshService; import org.runimo.runimo.auth.service.login.apple.AppleTokenVerifier; import org.runimo.runimo.user.domain.AppleUserToken; import org.runimo.runimo.user.domain.OAuthInfo; @@ -23,6 +24,7 @@ public class WithdrawService { private final AppleTokenVerifier appleTokenVerifier; private final AppleUserTokenRepository appleUserTokenRepository; private final EncryptUtil encryptUtil; + private final TokenRefreshService tokenRefreshService; @Transactional public void withdraw(Long userId) { @@ -34,6 +36,7 @@ public void withdraw(Long userId) { } oAuthInfoRepository.delete(oAuthInfo); userRepository.delete(user); + tokenRefreshService.removeRefreshToken(user.getId()); } private void withdrawAppleUser(User user) { From 8a7d8da11d586a1a95a00e8ee08d2142e95ecb32 Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Mon, 19 May 2025 01:28:44 +0900 Subject: [PATCH 04/21] :rocket: feat : make log out api --- .../usecases/logout/LogOutUsecase.java | 6 +++++ .../usecases/logout/LogOutUsecaseImpl.java | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 src/main/java/org/runimo/runimo/user/service/usecases/logout/LogOutUsecase.java create mode 100644 src/main/java/org/runimo/runimo/user/service/usecases/logout/LogOutUsecaseImpl.java diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/logout/LogOutUsecase.java b/src/main/java/org/runimo/runimo/user/service/usecases/logout/LogOutUsecase.java new file mode 100644 index 0000000..74cd0b3 --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/usecases/logout/LogOutUsecase.java @@ -0,0 +1,6 @@ +package org.runimo.runimo.user.service.usecases.logout; + +public interface LogOutUsecase { + + void execute(Long userId); +} diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/logout/LogOutUsecaseImpl.java b/src/main/java/org/runimo/runimo/user/service/usecases/logout/LogOutUsecaseImpl.java new file mode 100644 index 0000000..5e6c8af --- /dev/null +++ b/src/main/java/org/runimo/runimo/user/service/usecases/logout/LogOutUsecaseImpl.java @@ -0,0 +1,24 @@ +package org.runimo.runimo.user.service.usecases.logout; + +import lombok.RequiredArgsConstructor; +import org.runimo.runimo.auth.service.TokenRefreshService; +import org.runimo.runimo.user.domain.User; +import org.runimo.runimo.user.enums.UserHttpResponseCode; +import org.runimo.runimo.user.exception.UserException; +import org.runimo.runimo.user.service.UserFinder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LogOutUsecaseImpl implements LogOutUsecase { + + private final UserFinder userFinder; + private final TokenRefreshService tokenRefreshService; + + @Override + public void execute(Long userId) { + User user = userFinder.findUserById(userId).orElseThrow(() -> UserException.of( + UserHttpResponseCode.USER_NOT_FOUND)); + tokenRefreshService.removeRefreshToken(user.getId()); + } +} From f227816f05357319183367ffacf0928772da7174 Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Mon, 19 May 2025 01:29:20 +0900 Subject: [PATCH 05/21] :white_check_mark: test : add test for log out api --- .../user/controller/LogOutControllerTest.java | 69 +++++++++++++++++++ src/test/resources/sql/log_out_test_data.sql | 7 ++ 2 files changed, 76 insertions(+) create mode 100644 src/test/java/org/runimo/runimo/user/controller/LogOutControllerTest.java create mode 100644 src/test/resources/sql/log_out_test_data.sql diff --git a/src/test/java/org/runimo/runimo/user/controller/LogOutControllerTest.java b/src/test/java/org/runimo/runimo/user/controller/LogOutControllerTest.java new file mode 100644 index 0000000..78cd074 --- /dev/null +++ b/src/test/java/org/runimo/runimo/user/controller/LogOutControllerTest.java @@ -0,0 +1,69 @@ +package org.runimo.runimo.user.controller; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.*; +import static org.runimo.runimo.TestConsts.TEST_USER_UUID; + +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.runimo.runimo.CleanUpUtil; +import org.runimo.runimo.TokenUtils; +import org.runimo.runimo.exceptions.code.CustomResponseCode; +import org.runimo.runimo.user.enums.UserHttpResponseCode; +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.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; + + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +class LogOutControllerTest { + + @LocalServerPort + int port; + + @Autowired + private CleanUpUtil cleanUpUtil; + + @Autowired + private TokenUtils tokenUtils; + + private String token; + + @BeforeEach + void setUp() { + RestAssured.port = port; + token = tokenUtils.createTokenByUserPublicId(TEST_USER_UUID); + } + + @AfterEach + void tearDown() { + cleanUpUtil.cleanUpUserInfos(); + } + + @Test + @Sql(scripts = "/sql/log_out_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void logOut() { + CustomResponseCode responseCode = UserHttpResponseCode.LOG_OUT_SUCCESS; + + given() + .header("Authorization", token) + .contentType(ContentType.JSON) + + .when() + .post("/api/v1/users/log-out") + + .then() + .log().all() + .statusCode(responseCode.getHttpStatusCode().value()) + + .body("code", equalTo(responseCode.getCode())) + .body("message", equalTo(responseCode.getClientMessage())); + } +} \ No newline at end of file diff --git a/src/test/resources/sql/log_out_test_data.sql b/src/test/resources/sql/log_out_test_data.sql new file mode 100644 index 0000000..994f7fe --- /dev/null +++ b/src/test/resources/sql/log_out_test_data.sql @@ -0,0 +1,7 @@ +-- 사용자 +SET FOREIGN_KEY_CHECKS = 0; +TRUNCATE TABLE users; +INSERT INTO users (id, public_id, nickname, img_url, total_distance_in_meters, total_time_in_seconds, created_at, + updated_at) +VALUES (1, 'test-user-uuid-1', 'Daniel', 'https://example.com/images/user1.png', 10000, 3600, NOW(), NOW()); +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file From e1637d4479629d982e935b03972b620c2e9f5376 Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Mon, 19 May 2025 21:28:52 +0900 Subject: [PATCH 06/21] :white_check_mark: test : refactor healthCheck and logOut test code --- .../runimo/checker/controller/HealthCheckControllerTest.java | 4 ---- .../LogOutAcceptanceTest.java} | 5 ++--- 2 files changed, 2 insertions(+), 7 deletions(-) rename src/test/java/org/runimo/runimo/user/{controller/LogOutControllerTest.java => api/LogOutAcceptanceTest.java} (94%) diff --git a/src/test/java/org/runimo/runimo/checker/controller/HealthCheckControllerTest.java b/src/test/java/org/runimo/runimo/checker/controller/HealthCheckControllerTest.java index 45f36ba..f90e9ee 100644 --- a/src/test/java/org/runimo/runimo/checker/controller/HealthCheckControllerTest.java +++ b/src/test/java/org/runimo/runimo/checker/controller/HealthCheckControllerTest.java @@ -23,15 +23,11 @@ class HealthCheckControllerTest { @LocalServerPort int port; - @Autowired - private CleanUpUtil cleanUpUtil; - @BeforeEach void setUp() { RestAssured.port = port; } - @Test void healthCheck() { diff --git a/src/test/java/org/runimo/runimo/user/controller/LogOutControllerTest.java b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java similarity index 94% rename from src/test/java/org/runimo/runimo/user/controller/LogOutControllerTest.java rename to src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java index 78cd074..51e805f 100644 --- a/src/test/java/org/runimo/runimo/user/controller/LogOutControllerTest.java +++ b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java @@ -1,8 +1,7 @@ -package org.runimo.runimo.user.controller; +package org.runimo.runimo.user.api; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.equalTo; -import static org.junit.jupiter.api.Assertions.*; import static org.runimo.runimo.TestConsts.TEST_USER_UUID; import io.restassured.RestAssured; @@ -23,7 +22,7 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("test") -class LogOutControllerTest { +class LogOutAcceptanceTest { @LocalServerPort int port; From 6d9a58fef1cf1a6e9d99390af0602cd36ecfd215 Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Tue, 20 May 2025 02:12:16 +0900 Subject: [PATCH 07/21] :recycle: refactor : change log out api to endpoint /auth and move package --- .../runimo/{user => auth}/controller/LogOutController.java | 7 ++++--- .../usecases => auth/service}/logout/LogOutUsecase.java | 2 +- .../service}/logout/LogOutUsecaseImpl.java | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) rename src/main/java/org/runimo/runimo/{user => auth}/controller/LogOutController.java (88%) rename src/main/java/org/runimo/runimo/{user/service/usecases => auth/service}/logout/LogOutUsecase.java (54%) rename src/main/java/org/runimo/runimo/{user/service/usecases => auth/service}/logout/LogOutUsecaseImpl.java (93%) diff --git a/src/main/java/org/runimo/runimo/user/controller/LogOutController.java b/src/main/java/org/runimo/runimo/auth/controller/LogOutController.java similarity index 88% rename from src/main/java/org/runimo/runimo/user/controller/LogOutController.java rename to src/main/java/org/runimo/runimo/auth/controller/LogOutController.java index 54fa13a..afa1555 100644 --- a/src/main/java/org/runimo/runimo/user/controller/LogOutController.java +++ b/src/main/java/org/runimo/runimo/auth/controller/LogOutController.java @@ -1,4 +1,4 @@ -package org.runimo.runimo.user.controller; +package org.runimo.runimo.auth.controller; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -6,8 +6,9 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.runimo.runimo.common.response.SuccessResponse; +import org.runimo.runimo.user.controller.UserId; import org.runimo.runimo.user.enums.UserHttpResponseCode; -import org.runimo.runimo.user.service.usecases.logout.LogOutUsecase; +import org.runimo.runimo.auth.service.logout.LogOutUsecase; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -15,7 +16,7 @@ @Tag(name = "USER-LOG-OUT", description = "로그아웃 API") @RestController -@RequestMapping("/api/v1/users") +@RequestMapping("/api/v1/auth") @RequiredArgsConstructor public class LogOutController { diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/logout/LogOutUsecase.java b/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecase.java similarity index 54% rename from src/main/java/org/runimo/runimo/user/service/usecases/logout/LogOutUsecase.java rename to src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecase.java index 74cd0b3..e5497cc 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/logout/LogOutUsecase.java +++ b/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecase.java @@ -1,4 +1,4 @@ -package org.runimo.runimo.user.service.usecases.logout; +package org.runimo.runimo.auth.service.logout; public interface LogOutUsecase { diff --git a/src/main/java/org/runimo/runimo/user/service/usecases/logout/LogOutUsecaseImpl.java b/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecaseImpl.java similarity index 93% rename from src/main/java/org/runimo/runimo/user/service/usecases/logout/LogOutUsecaseImpl.java rename to src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecaseImpl.java index 5e6c8af..ae0e608 100644 --- a/src/main/java/org/runimo/runimo/user/service/usecases/logout/LogOutUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecaseImpl.java @@ -1,4 +1,4 @@ -package org.runimo.runimo.user.service.usecases.logout; +package org.runimo.runimo.auth.service.logout; import lombok.RequiredArgsConstructor; import org.runimo.runimo.auth.service.TokenRefreshService; From 7884c955a9044d9a13d721b2bbc826f48ff440c0 Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Tue, 20 May 2025 02:36:55 +0900 Subject: [PATCH 08/21] :recycle: refactor : change get token logic of log out api --- .../auth/controller/LogOutController.java | 10 +++++---- .../auth/service/TokenRefreshService.java | 22 +++++++++++++------ .../auth/service/logout/LogOutUsecase.java | 2 +- .../service/logout/LogOutUsecaseImpl.java | 6 +++-- .../runimo/user/api/LogOutAcceptanceTest.java | 4 ++-- 5 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/runimo/runimo/auth/controller/LogOutController.java b/src/main/java/org/runimo/runimo/auth/controller/LogOutController.java index afa1555..9a0be5f 100644 --- a/src/main/java/org/runimo/runimo/auth/controller/LogOutController.java +++ b/src/main/java/org/runimo/runimo/auth/controller/LogOutController.java @@ -6,15 +6,15 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.runimo.runimo.common.response.SuccessResponse; -import org.runimo.runimo.user.controller.UserId; import org.runimo.runimo.user.enums.UserHttpResponseCode; import org.runimo.runimo.auth.service.logout.LogOutUsecase; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -@Tag(name = "USER-LOG-OUT", description = "로그아웃 API") +@Tag(name = "LOG-OUT", description = "로그아웃 API") @RestController @RequestMapping("/api/v1/auth") @RequiredArgsConstructor @@ -25,13 +25,15 @@ public class LogOutController { @Operation(summary = "로그아웃", description = "사용자를 로그아웃 처리합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "로그아웃 성공"), + @ApiResponse(responseCode = "401", description = "토큰 검증 실패"), @ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음") }) @PostMapping("/log-out") public ResponseEntity> logOut( - @UserId Long userId + @RequestHeader("Authorization") String accessTokenHeader ) { - logOutUsecase.execute(userId); + String token = accessTokenHeader.replace("Bearer ", ""); + logOutUsecase.execute(token); return ResponseEntity.ok() .body(SuccessResponse.of(UserHttpResponseCode.LOG_OUT_SUCCESS, null)); } diff --git a/src/main/java/org/runimo/runimo/auth/service/TokenRefreshService.java b/src/main/java/org/runimo/runimo/auth/service/TokenRefreshService.java index a5b74d4..2934103 100644 --- a/src/main/java/org/runimo/runimo/auth/service/TokenRefreshService.java +++ b/src/main/java/org/runimo/runimo/auth/service/TokenRefreshService.java @@ -27,13 +27,7 @@ public void putRefreshToken(String userPublicId, String refreshToken) { } public TokenPair refreshAccessToken(String refreshToken) { - String userPublicId; - try { - jwtResolver.verifyJwtToken(refreshToken); - userPublicId = jwtResolver.getUserIdFromJwtToken(refreshToken); - } catch (Exception e) { - throw UserJwtException.of(UserHttpResponseCode.TOKEN_REFRESH_FAIL); - } + String userPublicId = getUserPublicIdFromJwt(refreshToken); User user = userFinder.findUserByPublicId(userPublicId) .orElseThrow(() -> UserJwtException.of(UserHttpResponseCode.TOKEN_REFRESH_FAIL)); @@ -49,6 +43,20 @@ public TokenPair refreshAccessToken(String refreshToken) { return new TokenPair(newAccessToken, refreshToken); } + /** + * JWT token 검증 및 사용자 정보 추출 + */ + public String getUserPublicIdFromJwt(String token) { + String userPublicId; + try { + jwtResolver.verifyJwtToken(token); + userPublicId = jwtResolver.getUserIdFromJwtToken(token); + } catch (Exception e) { + throw UserJwtException.of(UserHttpResponseCode.TOKEN_INVALID); + } + return userPublicId; + } + /** * 해당 사용자의 refresh token 삭제 * diff --git a/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecase.java b/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecase.java index e5497cc..a05f526 100644 --- a/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecase.java +++ b/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecase.java @@ -2,5 +2,5 @@ public interface LogOutUsecase { - void execute(Long userId); + void execute(String refreshToken); } diff --git a/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecaseImpl.java b/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecaseImpl.java index ae0e608..f4f7b76 100644 --- a/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecaseImpl.java @@ -16,9 +16,11 @@ public class LogOutUsecaseImpl implements LogOutUsecase { private final TokenRefreshService tokenRefreshService; @Override - public void execute(Long userId) { - User user = userFinder.findUserById(userId).orElseThrow(() -> UserException.of( + public void execute(String accessToken) { + String userPublicId = tokenRefreshService.getUserPublicIdFromJwt(accessToken); + User user = userFinder.findUserByPublicId(userPublicId).orElseThrow(() -> UserException.of( UserHttpResponseCode.USER_NOT_FOUND)); + tokenRefreshService.removeRefreshToken(user.getId()); } } diff --git a/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java index 51e805f..cd060d5 100644 --- a/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java +++ b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java @@ -48,7 +48,7 @@ void tearDown() { @Test @Sql(scripts = "/sql/log_out_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) - void logOut() { + void 로그아웃_성공() { CustomResponseCode responseCode = UserHttpResponseCode.LOG_OUT_SUCCESS; given() @@ -56,7 +56,7 @@ void logOut() { .contentType(ContentType.JSON) .when() - .post("/api/v1/users/log-out") + .post("/api/v1/auth/log-out") .then() .log().all() From f8996256a6b39ffa9dcc028094fccc588b25c2fd Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Wed, 21 May 2025 00:46:09 +0900 Subject: [PATCH 09/21] :recycle: refactor : add exception logic in JwtResolver.getUserIdFromJwtToken method --- .../java/org/runimo/runimo/auth/jwt/JwtResolver.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/runimo/runimo/auth/jwt/JwtResolver.java b/src/main/java/org/runimo/runimo/auth/jwt/JwtResolver.java index b622653..00dad9b 100644 --- a/src/main/java/org/runimo/runimo/auth/jwt/JwtResolver.java +++ b/src/main/java/org/runimo/runimo/auth/jwt/JwtResolver.java @@ -29,8 +29,14 @@ public UserDetail getUserDetailFromJwtToken(String token) throws JWTVerification } public String getUserIdFromJwtToken(String token) throws JWTVerificationException { - DecodedJWT jwt = verifyJwtToken(token); - return jwt.getSubject(); + String userPublicId; + try { + DecodedJWT jwt = verifyJwtToken(token); + userPublicId = jwt.getSubject(); + } catch (Exception e) { + throw UserJwtException.of(UserHttpResponseCode.TOKEN_INVALID); + } + return userPublicId; } public SignupTokenPayload getSignupTokenPayload(String token) From 798ae75a14d1d2e913be90d9dc93f02c6862423c Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Wed, 21 May 2025 00:46:55 +0900 Subject: [PATCH 10/21] :recycle: refactor : change token resolve logic at Logout and TokenRefresh api --- .../runimo/auth/service/TokenRefreshService.java | 16 +--------------- .../auth/service/logout/LogOutUsecaseImpl.java | 4 +++- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/runimo/runimo/auth/service/TokenRefreshService.java b/src/main/java/org/runimo/runimo/auth/service/TokenRefreshService.java index 2934103..f254204 100644 --- a/src/main/java/org/runimo/runimo/auth/service/TokenRefreshService.java +++ b/src/main/java/org/runimo/runimo/auth/service/TokenRefreshService.java @@ -27,7 +27,7 @@ public void putRefreshToken(String userPublicId, String refreshToken) { } public TokenPair refreshAccessToken(String refreshToken) { - String userPublicId = getUserPublicIdFromJwt(refreshToken); + String userPublicId = jwtResolver.getUserIdFromJwtToken(refreshToken); User user = userFinder.findUserByPublicId(userPublicId) .orElseThrow(() -> UserJwtException.of(UserHttpResponseCode.TOKEN_REFRESH_FAIL)); @@ -43,20 +43,6 @@ public TokenPair refreshAccessToken(String refreshToken) { return new TokenPair(newAccessToken, refreshToken); } - /** - * JWT token 검증 및 사용자 정보 추출 - */ - public String getUserPublicIdFromJwt(String token) { - String userPublicId; - try { - jwtResolver.verifyJwtToken(token); - userPublicId = jwtResolver.getUserIdFromJwtToken(token); - } catch (Exception e) { - throw UserJwtException.of(UserHttpResponseCode.TOKEN_INVALID); - } - return userPublicId; - } - /** * 해당 사용자의 refresh token 삭제 * diff --git a/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecaseImpl.java b/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecaseImpl.java index f4f7b76..ce43aba 100644 --- a/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecaseImpl.java @@ -1,6 +1,7 @@ package org.runimo.runimo.auth.service.logout; import lombok.RequiredArgsConstructor; +import org.runimo.runimo.auth.jwt.JwtResolver; import org.runimo.runimo.auth.service.TokenRefreshService; import org.runimo.runimo.user.domain.User; import org.runimo.runimo.user.enums.UserHttpResponseCode; @@ -14,10 +15,11 @@ public class LogOutUsecaseImpl implements LogOutUsecase { private final UserFinder userFinder; private final TokenRefreshService tokenRefreshService; + private final JwtResolver jwtResolver; @Override public void execute(String accessToken) { - String userPublicId = tokenRefreshService.getUserPublicIdFromJwt(accessToken); + String userPublicId = jwtResolver.getUserIdFromJwtToken(accessToken); User user = userFinder.findUserByPublicId(userPublicId).orElseThrow(() -> UserException.of( UserHttpResponseCode.USER_NOT_FOUND)); From cb16509d7acdfe0b17a3ab10db7e47bf4bbc9d35 Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Sat, 24 May 2025 19:48:12 +0900 Subject: [PATCH 11/21] :bulb: chore : tidy up useless imports --- .../runimo/checker/controller/HealthCheckControllerTest.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/test/java/org/runimo/runimo/checker/controller/HealthCheckControllerTest.java b/src/test/java/org/runimo/runimo/checker/controller/HealthCheckControllerTest.java index f90e9ee..608c9c5 100644 --- a/src/test/java/org/runimo/runimo/checker/controller/HealthCheckControllerTest.java +++ b/src/test/java/org/runimo/runimo/checker/controller/HealthCheckControllerTest.java @@ -2,16 +2,11 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.notNullValue; -import static org.junit.jupiter.api.Assertions.*; import io.restassured.RestAssured; import io.restassured.http.ContentType; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.runimo.runimo.CleanUpUtil; -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.test.context.ActiveProfiles; From 6c924a8c6f8b352d1f2e30d6927aeea206d66ce3 Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Sun, 25 May 2025 02:04:58 +0900 Subject: [PATCH 12/21] :white_check_mark: test : make log out test data sql --- src/test/resources/sql/log_out_test_data.sql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/resources/sql/log_out_test_data.sql b/src/test/resources/sql/log_out_test_data.sql index 994f7fe..97809d7 100644 --- a/src/test/resources/sql/log_out_test_data.sql +++ b/src/test/resources/sql/log_out_test_data.sql @@ -4,4 +4,8 @@ TRUNCATE TABLE users; INSERT INTO users (id, public_id, nickname, img_url, total_distance_in_meters, total_time_in_seconds, created_at, updated_at) VALUES (1, 'test-user-uuid-1', 'Daniel', 'https://example.com/images/user1.png', 10000, 3600, NOW(), NOW()); -SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file +SET FOREIGN_KEY_CHECKS = 1; + + +INSERT INTO oauth_account (id, user_id, provider, provider_id, created_at, updated_at) +VALUES (1, 1, 'KAKAO', 'test-oidc-token-1', NOW(), NOW()); \ No newline at end of file From bf76853c14215c6d7099a548f8f471a928aade74 Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Sun, 25 May 2025 02:06:08 +0900 Subject: [PATCH 13/21] :white_check_mark: test : make login-logout success test case --- .../runimo/user/api/LogOutAcceptanceTest.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java index cd060d5..62c481d 100644 --- a/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java +++ b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java @@ -1,38 +1,79 @@ package org.runimo.runimo.user.api; import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.ArgumentMatchers.any; import static org.runimo.runimo.TestConsts.TEST_USER_UUID; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import groovy.util.logging.Slf4j; import io.restassured.RestAssured; import io.restassured.http.ContentType; +import io.restassured.response.ValidatableResponse; +import org.checkerframework.checker.units.qual.C; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.runimo.runimo.CleanUpUtil; import org.runimo.runimo.TokenUtils; +import org.runimo.runimo.auth.controller.request.AuthSignupRequest; +import org.runimo.runimo.auth.controller.request.KakaoLoginRequest; +import org.runimo.runimo.auth.jwt.JwtTokenFactory; +import org.runimo.runimo.auth.service.TokenRefreshService; +import org.runimo.runimo.auth.service.dto.AuthResult; +import org.runimo.runimo.auth.service.dto.AuthStatus; +import org.runimo.runimo.auth.service.dto.TokenPair; +import org.runimo.runimo.auth.service.login.kakao.KakaoLoginHandler; import org.runimo.runimo.exceptions.code.CustomResponseCode; +import org.runimo.runimo.user.domain.Gender; +import org.runimo.runimo.user.domain.User; import org.runimo.runimo.user.enums.UserHttpResponseCode; +import org.runimo.runimo.user.repository.UserRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; 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.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.web.servlet.MockMvc; +@Slf4j @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("test") class LogOutAcceptanceTest { + private static final Logger log = LoggerFactory.getLogger(LogOutAcceptanceTest.class); @LocalServerPort int port; + @MockitoBean + private KakaoLoginHandler kakaoLoginHandler; + + @Autowired + private JwtTokenFactory jwtTokenFactory; + + @Autowired + private UserRepository userRepository; + + @Autowired + private TokenRefreshService tokenRefreshService; + @Autowired private CleanUpUtil cleanUpUtil; @Autowired private TokenUtils tokenUtils; + @Autowired + private ObjectMapper objectMapper; + private String token; @BeforeEach @@ -65,4 +106,56 @@ void tearDown() { .body("code", equalTo(responseCode.getCode())) .body("message", equalTo(responseCode.getClientMessage())); } + + + @Test + @Sql(scripts = "/sql/log_out_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void 카카오_로그인_후_로그아웃_성공_200() throws JsonProcessingException { + // 카카오 토큰 처리 mocking + User user = userRepository.findById(1L).orElseThrow(); + TokenPair tokenPair = jwtTokenFactory.generateTokenPair(user); + tokenRefreshService.putRefreshToken(user.getPublicId(), tokenPair.refreshToken()); + + Mockito.when(kakaoLoginHandler.validateAndLogin(any())) + .thenReturn(AuthResult.success(AuthStatus.LOGIN_SUCCESS, user, tokenPair)); + + // 로그인 + KakaoLoginRequest loginReq = new KakaoLoginRequest("test-oidc-token-1"); + + UserHttpResponseCode loginSuccessCode = UserHttpResponseCode.LOGIN_SUCCESS; + ValidatableResponse loginRes = given() + .body(objectMapper.writeValueAsString(loginReq)) + .contentType(ContentType.JSON) + + .when() + .post("/api/v1/auth/kakao") + + .then() + .log().all() + .statusCode(loginSuccessCode.getHttpStatusCode().value()) + + .body("code", equalTo(loginSuccessCode.getCode())) + .body("message", equalTo(loginSuccessCode.getClientMessage())) + .body("payload.access_token", notNullValue()) + .body("payload.refresh_token", notNullValue()) + .body("payload.img_url", notNullValue()); + + String accessToken = loginRes.extract().body().path("payload.access_token"); + + // 로그아웃 + CustomResponseCode logOutSuccessCode = UserHttpResponseCode.LOG_OUT_SUCCESS; + given() + .header("Authorization", accessToken) //"Bearer " + + .contentType(ContentType.JSON) + + .when() + .post("/api/v1/auth/log-out") + + .then() + .log().all() + .statusCode(logOutSuccessCode.getHttpStatusCode().value()) + + .body("code", equalTo(logOutSuccessCode.getCode())) + .body("message", equalTo(logOutSuccessCode.getClientMessage())); + } } \ No newline at end of file From 0c65ab3443fbfbd3be9a0bc167655cfb024f18f2 Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Sun, 25 May 2025 02:18:55 +0900 Subject: [PATCH 14/21] :bulb: chore : tidy up code --- .../runimo/user/api/LogOutAcceptanceTest.java | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java index 62c481d..7506d7f 100644 --- a/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java +++ b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java @@ -1,7 +1,6 @@ package org.runimo.runimo.user.api; import static io.restassured.RestAssured.given; -import static io.restassured.RestAssured.when; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; import static org.mockito.ArgumentMatchers.any; @@ -9,18 +8,15 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import groovy.util.logging.Slf4j; import io.restassured.RestAssured; import io.restassured.http.ContentType; import io.restassured.response.ValidatableResponse; -import org.checkerframework.checker.units.qual.C; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.runimo.runimo.CleanUpUtil; import org.runimo.runimo.TokenUtils; -import org.runimo.runimo.auth.controller.request.AuthSignupRequest; import org.runimo.runimo.auth.controller.request.KakaoLoginRequest; import org.runimo.runimo.auth.jwt.JwtTokenFactory; import org.runimo.runimo.auth.service.TokenRefreshService; @@ -29,27 +25,21 @@ import org.runimo.runimo.auth.service.dto.TokenPair; import org.runimo.runimo.auth.service.login.kakao.KakaoLoginHandler; import org.runimo.runimo.exceptions.code.CustomResponseCode; -import org.runimo.runimo.user.domain.Gender; import org.runimo.runimo.user.domain.User; import org.runimo.runimo.user.enums.UserHttpResponseCode; import org.runimo.runimo.user.repository.UserRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; 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.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.jdbc.Sql; -import org.springframework.test.web.servlet.MockMvc; -@Slf4j @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("test") class LogOutAcceptanceTest { - private static final Logger log = LoggerFactory.getLogger(LogOutAcceptanceTest.class); @LocalServerPort int port; @@ -58,10 +48,8 @@ class LogOutAcceptanceTest { @Autowired private JwtTokenFactory jwtTokenFactory; - @Autowired private UserRepository userRepository; - @Autowired private TokenRefreshService tokenRefreshService; @@ -145,7 +133,7 @@ void tearDown() { // 로그아웃 CustomResponseCode logOutSuccessCode = UserHttpResponseCode.LOG_OUT_SUCCESS; given() - .header("Authorization", accessToken) //"Bearer " + + .header("Authorization", accessToken) .contentType(ContentType.JSON) .when() From e9179ea33a169aed296c90bb1a18cf7caf9b4a68 Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Sun, 25 May 2025 15:46:39 +0900 Subject: [PATCH 15/21] :recycle: refactor : extract method of login-logout test case code --- .../runimo/user/api/LogOutAcceptanceTest.java | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java index 7506d7f..6685eed 100644 --- a/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java +++ b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java @@ -99,13 +99,11 @@ void tearDown() { @Test @Sql(scripts = "/sql/log_out_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) void 카카오_로그인_후_로그아웃_성공_200() throws JsonProcessingException { - // 카카오 토큰 처리 mocking - User user = userRepository.findById(1L).orElseThrow(); - TokenPair tokenPair = jwtTokenFactory.generateTokenPair(user); - tokenRefreshService.putRefreshToken(user.getPublicId(), tokenPair.refreshToken()); + // OIDC 토큰 처리 mocking + AuthResult authResult = createAuthResultOfTestUser(); Mockito.when(kakaoLoginHandler.validateAndLogin(any())) - .thenReturn(AuthResult.success(AuthStatus.LOGIN_SUCCESS, user, tokenPair)); + .thenReturn(authResult); // 로그인 KakaoLoginRequest loginReq = new KakaoLoginRequest("test-oidc-token-1"); @@ -146,4 +144,30 @@ void tearDown() { .body("code", equalTo(logOutSuccessCode.getCode())) .body("message", equalTo(logOutSuccessCode.getClientMessage())); } + + @Test + void 로그아웃_성공_이미_로그아웃된_사용자() { // refresh token 존재 안함 + + } + + @Test + void 로그아웃_실패_사용자_정보_없음() { + + } + + private AuthResult createAuthResultOfTestUser() { + User user = getTestUser(); + TokenPair tokenPair = getTokenPair(user); + return AuthResult.success(AuthStatus.LOGIN_SUCCESS, user, tokenPair); + } + + private TokenPair getTokenPair(User user) { + TokenPair tokenPair = jwtTokenFactory.generateTokenPair(user); + tokenRefreshService.putRefreshToken(user.getPublicId(), tokenPair.refreshToken()); + return tokenPair; + } + + private User getTestUser() { + return userRepository.findById(1L).orElseThrow(); + } } \ No newline at end of file From 53ccea2f2f6239fc5b421e5ebc20126e667c308d Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Sun, 25 May 2025 16:29:58 +0900 Subject: [PATCH 16/21] :white_check_mark: test : make apple login-logout test case --- .../runimo/user/api/LogOutAcceptanceTest.java | 58 ++++++++++++++++--- src/test/resources/sql/log_out_test_data.sql | 8 ++- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java index 6685eed..deb14ec 100644 --- a/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java +++ b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java @@ -17,12 +17,14 @@ import org.mockito.Mockito; import org.runimo.runimo.CleanUpUtil; import org.runimo.runimo.TokenUtils; +import org.runimo.runimo.auth.controller.request.AppleLoginRequest; import org.runimo.runimo.auth.controller.request.KakaoLoginRequest; import org.runimo.runimo.auth.jwt.JwtTokenFactory; import org.runimo.runimo.auth.service.TokenRefreshService; import org.runimo.runimo.auth.service.dto.AuthResult; import org.runimo.runimo.auth.service.dto.AuthStatus; import org.runimo.runimo.auth.service.dto.TokenPair; +import org.runimo.runimo.auth.service.login.apple.AppleLoginHandler; import org.runimo.runimo.auth.service.login.kakao.KakaoLoginHandler; import org.runimo.runimo.exceptions.code.CustomResponseCode; import org.runimo.runimo.user.domain.User; @@ -45,6 +47,8 @@ class LogOutAcceptanceTest { @MockitoBean private KakaoLoginHandler kakaoLoginHandler; + @MockitoBean + private AppleLoginHandler appleLoginHandler; @Autowired private JwtTokenFactory jwtTokenFactory; @@ -100,7 +104,7 @@ void tearDown() { @Sql(scripts = "/sql/log_out_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) void 카카오_로그인_후_로그아웃_성공_200() throws JsonProcessingException { // OIDC 토큰 처리 mocking - AuthResult authResult = createAuthResultOfTestUser(); + AuthResult authResult = createAuthResultOfTestUser(1L); Mockito.when(kakaoLoginHandler.validateAndLogin(any())) .thenReturn(authResult); @@ -146,13 +150,53 @@ void tearDown() { } @Test - void 로그아웃_성공_이미_로그아웃된_사용자() { // refresh token 존재 안함 + @Sql(scripts = "/sql/log_out_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) + void 애플_로그인_후_로그아웃_성공_200() throws JsonProcessingException { + // OIDC 토큰 처리 mocking + AuthResult authResult = createAuthResultOfTestUser(2L); - } + Mockito.when(appleLoginHandler.validateAndLogin(any(), any())) + .thenReturn(authResult); - @Test - void 로그아웃_실패_사용자_정보_없음() { + // 로그인 + AppleLoginRequest loginReq = new AppleLoginRequest("test-auth-code-1", + "test-auth-verifier-1"); + UserHttpResponseCode loginSuccessCode = UserHttpResponseCode.LOGIN_SUCCESS; + ValidatableResponse loginRes = given() + .body(objectMapper.writeValueAsString(loginReq)) + .contentType(ContentType.JSON) + + .when() + .post("/api/v1/auth/apple") + + .then() + .log().all() + .statusCode(loginSuccessCode.getHttpStatusCode().value()) + + .body("code", equalTo(loginSuccessCode.getCode())) + .body("message", equalTo(loginSuccessCode.getClientMessage())) + .body("payload.access_token", notNullValue()) + .body("payload.refresh_token", notNullValue()) + .body("payload.img_url", notNullValue()); + + String accessToken = loginRes.extract().body().path("payload.access_token"); + + // 로그아웃 + CustomResponseCode logOutSuccessCode = UserHttpResponseCode.LOG_OUT_SUCCESS; + given() + .header("Authorization", accessToken) + .contentType(ContentType.JSON) + + .when() + .post("/api/v1/auth/log-out") + + .then() + .log().all() + .statusCode(logOutSuccessCode.getHttpStatusCode().value()) + + .body("code", equalTo(logOutSuccessCode.getCode())) + .body("message", equalTo(logOutSuccessCode.getClientMessage())); } private AuthResult createAuthResultOfTestUser() { @@ -167,7 +211,7 @@ private TokenPair getTokenPair(User user) { return tokenPair; } - private User getTestUser() { - return userRepository.findById(1L).orElseThrow(); + private User getTestUser(Long userId) { + return userRepository.findById(userId).orElseThrow(); } } \ No newline at end of file diff --git a/src/test/resources/sql/log_out_test_data.sql b/src/test/resources/sql/log_out_test_data.sql index 97809d7..7fe1187 100644 --- a/src/test/resources/sql/log_out_test_data.sql +++ b/src/test/resources/sql/log_out_test_data.sql @@ -3,9 +3,11 @@ SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE users; INSERT INTO users (id, public_id, nickname, img_url, total_distance_in_meters, total_time_in_seconds, created_at, updated_at) -VALUES (1, 'test-user-uuid-1', 'Daniel', 'https://example.com/images/user1.png', 10000, 3600, NOW(), NOW()); +VALUES (1, 'test-user-uuid-1', 'Daniel', 'https://example.com/images/user1.png', 10000, 3600, NOW(), NOW()), + (2, 'test-user-uuid-2', 'Daniel2', 'https://example.com/images/user2.png', 20000, 2600, NOW(), NOW()); SET FOREIGN_KEY_CHECKS = 1; - +TRUNCATE TABLE oauth_account; INSERT INTO oauth_account (id, user_id, provider, provider_id, created_at, updated_at) -VALUES (1, 1, 'KAKAO', 'test-oidc-token-1', NOW(), NOW()); \ No newline at end of file +VALUES (1, 1, 'KAKAO', 'test-oidc-token-1', NOW(), NOW()), + (2, 2, 'APPLE', 'test-oidc-token-2', NOW(), NOW()); \ No newline at end of file From 77b68b2427f0797ea176ea44f03629165a8d255b Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Sun, 25 May 2025 18:05:49 +0900 Subject: [PATCH 17/21] :sparkles: add : add 'already log out' response at log out api --- .../runimo/auth/controller/LogOutController.java | 8 ++++++-- .../runimo/auth/service/TokenRefreshService.java | 14 ++++++++++++-- .../runimo/auth/service/logout/LogOutUsecase.java | 2 +- .../auth/service/logout/LogOutUsecaseImpl.java | 12 +++++++++++- .../runimo/user/enums/UserHttpResponseCode.java | 1 + 5 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/runimo/runimo/auth/controller/LogOutController.java b/src/main/java/org/runimo/runimo/auth/controller/LogOutController.java index 9a0be5f..d0183cc 100644 --- a/src/main/java/org/runimo/runimo/auth/controller/LogOutController.java +++ b/src/main/java/org/runimo/runimo/auth/controller/LogOutController.java @@ -25,6 +25,7 @@ public class LogOutController { @Operation(summary = "로그아웃", description = "사용자를 로그아웃 처리합니다.") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "로그아웃 성공"), + @ApiResponse(responseCode = "200", description = "로그아웃 성공 (이미 로그아웃된 사용자)"), @ApiResponse(responseCode = "401", description = "토큰 검증 실패"), @ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음") }) @@ -33,8 +34,11 @@ public ResponseEntity> logOut( @RequestHeader("Authorization") String accessTokenHeader ) { String token = accessTokenHeader.replace("Bearer ", ""); - logOutUsecase.execute(token); + boolean logoutProcessed = logOutUsecase.execute(token); + + UserHttpResponseCode responseCode = logoutProcessed ? UserHttpResponseCode.LOG_OUT_SUCCESS + : UserHttpResponseCode.ALREADY_LOG_OUT_SUCCESS; return ResponseEntity.ok() - .body(SuccessResponse.of(UserHttpResponseCode.LOG_OUT_SUCCESS, null)); + .body(SuccessResponse.of(responseCode, null)); } } diff --git a/src/main/java/org/runimo/runimo/auth/service/TokenRefreshService.java b/src/main/java/org/runimo/runimo/auth/service/TokenRefreshService.java index f254204..0ab219c 100644 --- a/src/main/java/org/runimo/runimo/auth/service/TokenRefreshService.java +++ b/src/main/java/org/runimo/runimo/auth/service/TokenRefreshService.java @@ -33,8 +33,7 @@ public TokenPair refreshAccessToken(String refreshToken) { .orElseThrow(() -> UserJwtException.of(UserHttpResponseCode.TOKEN_REFRESH_FAIL)); // Check if the refresh token is expired - String storedToken = jwtTokenRepository.findRefreshTokenByUserId(user.getId()) - .orElseThrow(() -> UserJwtException.of(UserHttpResponseCode.REFRESH_EXPIRED)); + String storedToken = getStoredRefreshToken(user.getId()); if (!storedToken.equals(refreshToken)) { throw UserJwtException.of(UserHttpResponseCode.TOKEN_INVALID); } @@ -43,6 +42,17 @@ public TokenPair refreshAccessToken(String refreshToken) { return new TokenPair(newAccessToken, refreshToken); } + /** + * 해당 사용자의 refresh token 조회 + * + * @param userId 사용자 식별자 + * @return refresh 토큰 + */ + public String getStoredRefreshToken(Long userId) { + return jwtTokenRepository.findRefreshTokenByUserId(userId) + .orElseThrow(() -> UserJwtException.of(UserHttpResponseCode.REFRESH_EXPIRED)); + } + /** * 해당 사용자의 refresh token 삭제 * diff --git a/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecase.java b/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecase.java index a05f526..55f103f 100644 --- a/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecase.java +++ b/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecase.java @@ -2,5 +2,5 @@ public interface LogOutUsecase { - void execute(String refreshToken); + boolean execute(String refreshToken); } diff --git a/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecaseImpl.java b/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecaseImpl.java index ce43aba..32ab511 100644 --- a/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecaseImpl.java +++ b/src/main/java/org/runimo/runimo/auth/service/logout/LogOutUsecaseImpl.java @@ -1,6 +1,7 @@ package org.runimo.runimo.auth.service.logout; import lombok.RequiredArgsConstructor; +import org.runimo.runimo.auth.exceptions.UserJwtException; import org.runimo.runimo.auth.jwt.JwtResolver; import org.runimo.runimo.auth.service.TokenRefreshService; import org.runimo.runimo.user.domain.User; @@ -18,11 +19,20 @@ public class LogOutUsecaseImpl implements LogOutUsecase { private final JwtResolver jwtResolver; @Override - public void execute(String accessToken) { + public boolean execute(String accessToken) { String userPublicId = jwtResolver.getUserIdFromJwtToken(accessToken); User user = userFinder.findUserByPublicId(userPublicId).orElseThrow(() -> UserException.of( UserHttpResponseCode.USER_NOT_FOUND)); + try { + tokenRefreshService.getStoredRefreshToken(user.getId()); + } catch (UserJwtException ue) { + if (ue.getErrorCode() == UserHttpResponseCode.REFRESH_EXPIRED) { + return false; + } + } + tokenRefreshService.removeRefreshToken(user.getId()); + return true; } } diff --git a/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java b/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java index 15f545a..157ac11 100644 --- a/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java +++ b/src/main/java/org/runimo/runimo/user/enums/UserHttpResponseCode.java @@ -26,6 +26,7 @@ public enum UserHttpResponseCode implements CustomResponseCode { TOKEN_DELETE_REFRESH_FAIL(HttpStatus.FORBIDDEN, "토큰 삭제 실패", "사용자가 유효하지 않습니다. Refresh 토큰 삭제에 실패했습니다"), LOG_OUT_SUCCESS(HttpStatus.OK, "로그아웃 성공", "로그아웃 성공"), + ALREADY_LOG_OUT_SUCCESS(HttpStatus.OK, "로그아웃 성공 (이미 로그아웃된 사용자)", "로그아웃 성공 (이미 로그아웃된 사용자)"), USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없음", "사용자를 찾을 수 없음"); private final HttpStatus code; From a31d44a220c7ce69794f0685c86cbc7ae19adf9f Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Sun, 25 May 2025 18:06:15 +0900 Subject: [PATCH 18/21] :white_check_mark: test : modify test for already log out case --- .../runimo/user/api/LogOutAcceptanceTest.java | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java index deb14ec..e39166f 100644 --- a/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java +++ b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java @@ -81,8 +81,8 @@ void tearDown() { @Test @Sql(scripts = "/sql/log_out_test_data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) - void 로그아웃_성공() { - CustomResponseCode responseCode = UserHttpResponseCode.LOG_OUT_SUCCESS; + void 로그아웃_성공_이미_로그아웃된_사용자_200() { + CustomResponseCode responseCode = UserHttpResponseCode.ALREADY_LOG_OUT_SUCCESS; given() .header("Authorization", token) @@ -198,9 +198,34 @@ void tearDown() { .body("code", equalTo(logOutSuccessCode.getCode())) .body("message", equalTo(logOutSuccessCode.getClientMessage())); } - - private AuthResult createAuthResultOfTestUser() { - User user = getTestUser(); +// +// @Test +// void 로그아웃_실패_사용자_찾을_수_없음() { +// CustomResponseCode responseCode = UserHttpResponseCode.USER_NOT_FOUND; +// +// given() +// .header("Authorization", token) +// .contentType(ContentType.JSON) +// +// .when() +// .post("/api/v1/auth/log-out") +// +// .then() +// .log().all() +// .statusCode(responseCode.getHttpStatusCode().value()) +// +// .body("code", equalTo(responseCode.getCode())) +// .body("message", equalTo(responseCode.getClientMessage())); +// +// } +// +// @Test +// void 로그아웃_실패_토큰_인증_불가() { +// +// } + + private AuthResult createAuthResultOfTestUser(Long userId) { + User user = getTestUser(userId); TokenPair tokenPair = getTokenPair(user); return AuthResult.success(AuthStatus.LOGIN_SUCCESS, user, tokenPair); } From 314018ac86010647952b38ea8b9ba39da987bdb8 Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Sun, 25 May 2025 18:49:08 +0900 Subject: [PATCH 19/21] :bug: fix : add UserException handler --- .../runimo/runimo/exceptions/GlobalExceptionHandler.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/org/runimo/runimo/exceptions/GlobalExceptionHandler.java b/src/main/java/org/runimo/runimo/exceptions/GlobalExceptionHandler.java index ebc8bfd..7c249f4 100644 --- a/src/main/java/org/runimo/runimo/exceptions/GlobalExceptionHandler.java +++ b/src/main/java/org/runimo/runimo/exceptions/GlobalExceptionHandler.java @@ -10,6 +10,7 @@ import org.runimo.runimo.external.ExternalServiceException; import org.runimo.runimo.hatch.exception.HatchException; import org.runimo.runimo.runimo.exception.RunimoException; +import org.runimo.runimo.user.exception.UserException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; @@ -48,6 +49,13 @@ public ResponseEntity handleUserJwtException(UserJwtException e) .body(ErrorResponse.of(e.getErrorCode())); } + @ExceptionHandler(UserException.class) + public ResponseEntity handleUserJwtException(UserException e) { + log.warn("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); + return ResponseEntity.status(e.getHttpStatusCode()) + .body(ErrorResponse.of(e.getErrorCode())); + } + @ExceptionHandler(SignUpException.class) public ResponseEntity handleSignUpException(SignUpException e) { log.warn("{} {}", ERROR_LOG_HEADER, e.getMessage(), e); From 35464c3e5e7f3cd4106aca95625eb94b0763e609 Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Sun, 25 May 2025 19:05:30 +0900 Subject: [PATCH 20/21] :recycle: refactor : rename log out acceptance test class --- ....java => LogOutAcceptanceSuccessTest.java} | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) rename src/test/java/org/runimo/runimo/user/api/{LogOutAcceptanceTest.java => LogOutAcceptanceSuccessTest.java} (91%) diff --git a/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceSuccessTest.java similarity index 91% rename from src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java rename to src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceSuccessTest.java index e39166f..5995926 100644 --- a/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceTest.java +++ b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceSuccessTest.java @@ -40,7 +40,7 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("test") -class LogOutAcceptanceTest { +class LogOutAcceptanceSuccessTest { @LocalServerPort int port; @@ -198,31 +198,6 @@ void tearDown() { .body("code", equalTo(logOutSuccessCode.getCode())) .body("message", equalTo(logOutSuccessCode.getClientMessage())); } -// -// @Test -// void 로그아웃_실패_사용자_찾을_수_없음() { -// CustomResponseCode responseCode = UserHttpResponseCode.USER_NOT_FOUND; -// -// given() -// .header("Authorization", token) -// .contentType(ContentType.JSON) -// -// .when() -// .post("/api/v1/auth/log-out") -// -// .then() -// .log().all() -// .statusCode(responseCode.getHttpStatusCode().value()) -// -// .body("code", equalTo(responseCode.getCode())) -// .body("message", equalTo(responseCode.getClientMessage())); -// -// } -// -// @Test -// void 로그아웃_실패_토큰_인증_불가() { -// -// } private AuthResult createAuthResultOfTestUser(Long userId) { User user = getTestUser(userId); From 2bb7704f3f30f72dc7d6a23fd9bdb6cc4e3aa366 Mon Sep 17 00:00:00 2001 From: jeeheaG Date: Sun, 25 May 2025 22:16:54 +0900 Subject: [PATCH 21/21] :white_check_mark: test : make fail test cases for log out api --- .../user/api/LogOutAcceptanceFailTest.java | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceFailTest.java diff --git a/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceFailTest.java b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceFailTest.java new file mode 100644 index 0000000..423db43 --- /dev/null +++ b/src/test/java/org/runimo/runimo/user/api/LogOutAcceptanceFailTest.java @@ -0,0 +1,78 @@ +package org.runimo.runimo.user.api; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; + +import com.auth0.jwt.interfaces.DecodedJWT; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.runimo.runimo.auth.jwt.JwtResolver; +import org.runimo.runimo.exceptions.code.CustomResponseCode; +import org.runimo.runimo.user.enums.UserHttpResponseCode; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; + + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +class LogOutAcceptanceFailTest { + + @LocalServerPort + int port; + + @MockitoSpyBean + private JwtResolver jwtResolver; + + @BeforeEach + void setUp() { + RestAssured.port = port; + } + + @Test + void 로그아웃_실패_토큰_인증_불가() { + CustomResponseCode responseCode = UserHttpResponseCode.TOKEN_INVALID; + + given() + .header("Authorization", "--invalid token value--") + .contentType(ContentType.JSON) + + .when() + .post("/api/v1/auth/log-out") + + .then() + .log().all() + .statusCode(responseCode.getHttpStatusCode().value()) + + .body("code", equalTo(responseCode.getCode())) + .body("message", equalTo(responseCode.getClientMessage())); + } + + @Test + void 로그아웃_실패_사용자_찾을_수_없음() { + CustomResponseCode responseCode = UserHttpResponseCode.USER_NOT_FOUND; + doReturn("wrong user public id").when(jwtResolver).getUserIdFromJwtToken(any()); + + given() + .header("Authorization", "--some token value--") + .contentType(ContentType.JSON) + + .when() + .post("/api/v1/auth/log-out") + + .then() + .log().all() + .statusCode(responseCode.getHttpStatusCode().value()) + + .body("code", equalTo(responseCode.getCode())) + .body("message", equalTo(responseCode.getClientMessage())); + } + +} \ No newline at end of file