diff --git a/capturecat-core/compose.yml b/capturecat-core/compose.yml new file mode 100644 index 0000000..e38e90b --- /dev/null +++ b/capturecat-core/compose.yml @@ -0,0 +1,26 @@ +services: + dev-capturecat-db: + image: postgres + environment: + POSTGRES_DB: capturecat + POSTGRES_USER: capturecat + POSTGRES_PASSWORD: capturecat77 + MYSQL_ROOT_PASSWORD: + MYSQL_DATABASE: + volumes: + - /home/ubuntu/capturecat/database/docker-postgresql/postgresql_data:/var/lib/postgresql/data + ports: + - 5432:5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB} -h 127.0.0.1 -p 5432"] + interval: 5s + retries: 10 + dev-capturecat-cache-server: + image: redis + command: ["redis-server", "--requirepass", "capturecat77"] + ports: + - 6379:6379 + healthcheck: + test: [ "CMD", "redis-cli", "ping" ] + interval: 5s + retries: 10 diff --git a/capturecat-core/src/main/java/com/capturecat/core/config/SecurityConfig.java b/capturecat-core/src/main/java/com/capturecat/core/config/SecurityConfig.java index 07a8e65..0a2114e 100644 --- a/capturecat-core/src/main/java/com/capturecat/core/config/SecurityConfig.java +++ b/capturecat-core/src/main/java/com/capturecat/core/config/SecurityConfig.java @@ -46,15 +46,16 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http.csrf(AbstractHttpConfigurer::disable) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .formLogin(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) - .addFilterBefore(new JwtFilter(jwtUtil, tokenService), JwtLoginFilter.class) + .addFilterBefore(new JwtFilter(jwtUtil, tokenService), UsernamePasswordAuthenticationFilter.class) .addFilterAt( new JwtLoginFilter(authenticationManager(authenticationConfiguration), tokenService), UsernamePasswordAuthenticationFilter.class) - .addFilterAt(new JwtLogoutFilter(tokenService), LogoutFilter.class) + .addFilterBefore(new JwtLogoutFilter(tokenService), LogoutFilter.class) .authorizeHttpRequests( authorizeRequests -> authorizeRequests - .requestMatchers("/health", "/docs/**", "/token/reissue", "/v1/auth/**", "/v1/user/join") + .requestMatchers("/health", "/docs/**", "/token/reissue", "/v1/auth/**", "/v1/user/join", "/logout") .permitAll() .anyRequest() .hasRole("USER")); diff --git a/capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtFilter.java b/capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtFilter.java index 0750884..ad540e7 100644 --- a/capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtFilter.java +++ b/capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtFilter.java @@ -72,4 +72,9 @@ private void rejectInvalidToken(HttpServletResponse response, ErrorType errorTyp objectMapper.writeValue(response.getWriter(), ApiResponse.error(errorType)); } + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String path = request.getServletPath(); + return "/logout".equals(path); // 로그아웃은 스킵 + } } diff --git a/capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtLoginFilter.java b/capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtLoginFilter.java index cac8de9..73fc651 100644 --- a/capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtLoginFilter.java +++ b/capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtLoginFilter.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import com.capturecat.core.api.user.dto.UserReqDto.LoginReqDto; import com.capturecat.core.domain.user.UserRole; @@ -32,6 +33,7 @@ * 소셜 로그인/회원가입이 아닌, * 일반 회원가입 후 /login 경로로, id, password로 로그인한 경우 (개발 용도) */ +@Slf4j @RequiredArgsConstructor public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter { @@ -64,6 +66,7 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR //토큰 발급 Map tokenMap = tokenIssueService.issue(username, UserRole.fromRoleString(role)); + log.info("[JwtLoginFilter.successfulAuthentication] 사용자 로그인({}), 토큰 발급", username); //Header에 실어 응답 response.setHeader(HttpHeaders.AUTHORIZATION, JwtUtil.BEARER_PREFIX + tokenMap.get(TokenType.ACCESS)); diff --git a/capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtLogoutFilter.java b/capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtLogoutFilter.java index 906a37a..14fb56b 100644 --- a/capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtLogoutFilter.java +++ b/capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtLogoutFilter.java @@ -2,10 +2,8 @@ import java.io.IOException; +import jakarta.servlet.DispatcherType; import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -13,61 +11,66 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.util.StringUtils; -import org.springframework.web.filter.GenericFilterBean; +import org.springframework.web.filter.OncePerRequestFilter; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import com.capturecat.core.service.auth.TokenService; import com.capturecat.core.support.error.CoreException; import com.capturecat.core.support.error.ErrorType; import com.capturecat.core.support.response.ApiResponse; +@Slf4j @RequiredArgsConstructor -public class JwtLogoutFilter extends GenericFilterBean { +public class JwtLogoutFilter extends OncePerRequestFilter { private static final String LOGOUT_PATH = "/logout"; private final TokenService tokenService; private final ObjectMapper objectMapper = new ObjectMapper(); @Override - public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) - throws IOException, ServletException { - doFilter((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse, filterChain); - } - - /** 로그아웃 - * Refresh 토큰을 받아 DB에서 삭제 후 쿠키 null로 초기화하여 응답 - * (모든 기기에서 로그아웃 시 username 기반으로 모든 refresh 토큰 삭제) - */ - private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws IOException, ServletException { - - // 로그아웃 요청일 때 - if (!request.getRequestURI().equals(LOGOUT_PATH)) { - filterChain.doFilter(request, response); - return; + protected boolean shouldNotFilter(HttpServletRequest request) { + // 1) ERROR/ASYNC 등 요청 아닌 디스패치 스킵 + if (request.getDispatcherType() != DispatcherType.REQUEST) { + return true; } + // 2) POST만 허용 + if (!"POST".equalsIgnoreCase(request.getMethod())) { + return true; + } + // 3) /logout 경로만 통과 + return !LOGOUT_PATH.equals(request.getRequestURI()); + } - // 로그아웃 + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws IOException { try { String accessHeader = request.getHeader(HttpHeaders.AUTHORIZATION); String refreshHeader = request.getHeader(JwtUtil.REFRESH_TOKEN_HEADER); if (!StringUtils.hasText(accessHeader) || !StringUtils.hasText(refreshHeader)) { - throw new CoreException(ErrorType.INVALID_AUTH_TOKEN); + throw new CoreException(ErrorType.INVALID_LOGOUT_AUTH_TOKEN); } + log.info("LogoutFilter hit: dispatcher={}, path={}", request.getDispatcherType(), request.getServletPath()); + tokenService.revokeUserTokens(accessHeader, refreshHeader); + + response.setStatus(HttpStatus.OK.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + objectMapper.writeValue(response.getWriter(), ApiResponse.success()); } catch (CoreException e) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); objectMapper.writeValue(response.getWriter(), ApiResponse.error(e.getErrorType())); - return; + } catch (Exception e) { + response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + objectMapper.writeValue(response.getWriter(), + ApiResponse.error(ErrorType.INTERNAL_SERVER_ERROR)); } - - // 성공 응답 - response.setStatus(HttpStatus.OK.value()); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - objectMapper.writeValue(response.getWriter(), ApiResponse.success()); + // 체인 종료 (추가 필터로 넘기지 않음) } } diff --git a/capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtUtil.java b/capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtUtil.java index cf701ca..e344db7 100644 --- a/capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtUtil.java +++ b/capturecat-core/src/main/java/com/capturecat/core/config/jwt/JwtUtil.java @@ -17,6 +17,9 @@ import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.security.SignatureException; +import com.capturecat.core.support.error.CoreException; +import com.capturecat.core.support.error.ErrorType; + @Component public class JwtUtil { @@ -77,10 +80,11 @@ public boolean isValid(String token) { return true; } - public String resolveToken(String header) { - return header != null && header.startsWith(BEARER_PREFIX) - ? header.substring(BEARER_PREFIX.length()).trim() - : null; + public String resolveToken(String authHeader) { + if (authHeader == null || !authHeader.startsWith(JwtUtil.BEARER_PREFIX)) { + throw new CoreException(ErrorType.INVALID_ACCESS_TOKEN); + } + return authHeader.substring(BEARER_PREFIX.length()).trim(); } // JWT 파싱하여 만료일자(ms) 반환 @@ -89,7 +93,6 @@ public long getExpiration(String token) { return claims.getExpiration().getTime(); } - // username(subject) 추출 public String getUsername(String token) { return extractClaims(token).getSubject(); diff --git a/capturecat-core/src/main/java/com/capturecat/core/service/auth/TokenService.java b/capturecat-core/src/main/java/com/capturecat/core/service/auth/TokenService.java index 638d885..2243727 100644 --- a/capturecat-core/src/main/java/com/capturecat/core/service/auth/TokenService.java +++ b/capturecat-core/src/main/java/com/capturecat/core/service/auth/TokenService.java @@ -109,8 +109,12 @@ private void saveRefreshToken(String username, String refreshToken) { * Refresh Token 삭제 및 Access Token 블랙리스트 등록 */ public void revokeUserTokens(String accessTokenHeader, String refreshTokenHeader) { - blacklistAccessToken(accessTokenHeader); - deleteValidRefreshToken(refreshTokenHeader); + try { + blacklistAccessToken(accessTokenHeader); + deleteValidRefreshToken(refreshTokenHeader); + } catch (Exception e) { + throw new CoreException(ErrorType.INTERNAL_SERVER_ERROR); + } } /** @@ -145,6 +149,7 @@ public void blacklistAccessToken(String authHeader) { redisTemplate.opsForValue() .set(blacklistKey(accessToken), "blacklisted", remainMillis, TimeUnit.MILLISECONDS); } + log.info("Blacklist Token: {}", accessToken); } public boolean isBlacklisted(String accessToken) { diff --git a/capturecat-core/src/main/java/com/capturecat/core/service/user/UserService.java b/capturecat-core/src/main/java/com/capturecat/core/service/user/UserService.java index 7ec5a9b..a33a786 100644 --- a/capturecat-core/src/main/java/com/capturecat/core/service/user/UserService.java +++ b/capturecat-core/src/main/java/com/capturecat/core/service/user/UserService.java @@ -113,7 +113,6 @@ public String withdraw(LoginUser loginUser, String reason) { return resultMessage; } - @Transactional protected void deleteUserAndRelated(Long userId) { //1. 즐겨찾기 삭제 bookmarkRepository.deleteByUserId(userId); diff --git a/capturecat-core/src/main/java/com/capturecat/core/service/user/WithdrawLogService.java b/capturecat-core/src/main/java/com/capturecat/core/service/user/WithdrawLogService.java index 4d56d8e..91d3704 100644 --- a/capturecat-core/src/main/java/com/capturecat/core/service/user/WithdrawLogService.java +++ b/capturecat-core/src/main/java/com/capturecat/core/service/user/WithdrawLogService.java @@ -16,8 +16,7 @@ public class WithdrawLogService { private final WithdrawLogRepository withdrawLogRepository; - // 전파 시 실패해도 록백하지 않도록 - @Transactional(propagation = Propagation.REQUIRES_NEW) + @Transactional public void save(Long userId, String reason) { try { WithdrawLog log = WithdrawLog.builder() diff --git a/capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorCode.java b/capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorCode.java index 0c8f303..a31499b 100644 --- a/capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorCode.java +++ b/capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorCode.java @@ -36,7 +36,9 @@ public enum ErrorCode { UNLINK_SOCIAL_FAIL("소셜 로그인 연결 해제에 실패했습니다."), FETCH_SOCIAL_TOKEN_FAIL("소셜 서비스로부터 idToken 혹은 unlinkKey 획득에 실패했습니다."), SOCIAL_API_ERROR("소셜 서비스 API 호출 결과 실패를 응답받았습니다."), - MISSING_PARAMETER("필수 파라미터 %s(이)가 누락되었습니다."); + MISSING_PARAMETER("필수 파라미터 %s(이)가 누락되었습니다."), + INTERNAL_SERVER_ERROR("서버에서 오류가 발생했습니다."), + INVALID_LOGOUT_AUTH_TOKEN("ACCESS 토큰 또는 REFRESH 토큰이 유효하지 않습니다."); private final String message; diff --git a/capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorType.java b/capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorType.java index 5cf0217..d8f1b26 100644 --- a/capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorType.java +++ b/capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorType.java @@ -35,12 +35,14 @@ public enum ErrorType { BOOKMARK_DUPLICATION(HttpStatus.BAD_REQUEST, ErrorCode.ALREADY_EXISTS_BOOKMARK, LogLevel.WARN), INVALID_ID_TOKEN(HttpStatus.UNAUTHORIZED, ErrorCode.INVALID_ID_TOKEN, LogLevel.WARN), INVALID_AUTH_TOKEN(HttpStatus.UNAUTHORIZED, ErrorCode.INVALID_AUTH_TOKEN, LogLevel.WARN), + INVALID_LOGOUT_AUTH_TOKEN(HttpStatus.UNAUTHORIZED, ErrorCode.INVALID_LOGOUT_AUTH_TOKEN, LogLevel.WARN), BOOKMARK_NOT_FOUND(HttpStatus.NOT_FOUND, ErrorCode.NOT_FOUND_BOOKMARK, LogLevel.WARN), GENERATE_CLIENT_SECRET_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, ErrorCode.GENERATE_CLIENT_SECRET_FAIL, LogLevel.ERROR), UNLINK_SOCIAL_FAIL(HttpStatus.BAD_REQUEST, ErrorCode.UNLINK_SOCIAL_FAIL, LogLevel.WARN), FETCH_SOCIAL_TOKEN_FAIL(HttpStatus.BAD_REQUEST, ErrorCode.FETCH_SOCIAL_TOKEN_FAIL, LogLevel.WARN), SOCIAL_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, ErrorCode.SOCIAL_API_ERROR, LogLevel.WARN), - MISSING_PARAMETER(HttpStatus.BAD_REQUEST, ErrorCode.MISSING_PARAMETER, LogLevel.WARN),; + MISSING_PARAMETER(HttpStatus.BAD_REQUEST, ErrorCode.MISSING_PARAMETER, LogLevel.WARN), + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, ErrorCode.INTERNAL_SERVER_ERROR, LogLevel.ERROR); private final HttpStatus status; diff --git a/capturecat-core/src/main/resources/application.yml b/capturecat-core/src/main/resources/application.yml index 7f60b37..80e9420 100644 --- a/capturecat-core/src/main/resources/application.yml +++ b/capturecat-core/src/main/resources/application.yml @@ -28,7 +28,6 @@ logging: springframework.security: DEBUG jwt: - secret: capture_cat_jwt_secret_key_capture_cat_jwt_secret_key access-token-expiration: 3600000 refresh-token-expiration: 2592000000 diff --git a/capturecat-core/src/test/java/com/capturecat/core/api/auth/LogInOutTest.java b/capturecat-core/src/test/java/com/capturecat/core/api/auth/LogInOutTest.java index 37c254f..c6c2eea 100644 --- a/capturecat-core/src/test/java/com/capturecat/core/api/auth/LogInOutTest.java +++ b/capturecat-core/src/test/java/com/capturecat/core/api/auth/LogInOutTest.java @@ -9,6 +9,7 @@ import org.apache.http.HttpHeaders; import org.apache.http.HttpStatus; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; import org.springframework.restdocs.payload.JsonFieldType; @@ -37,25 +38,42 @@ void setUp() { } @Test + @DisplayName("POST /logout - 헤더에 토큰이 있으면 200 OK") void 로그아웃() { - //given + // given String accessTokenHeader = "valid-access-token-header"; String refreshTokenHeader = "valid-refresh-token-header"; willDoNothing().given(tokenService).revokeUserTokens(anyString(), anyString()); - //when & then + // when & then given() + .contentType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE) + .accept(org.springframework.http.MediaType.APPLICATION_JSON_VALUE) .header(HttpHeaders.AUTHORIZATION, JwtUtil.BEARER_PREFIX + accessTokenHeader) .header(JwtUtil.REFRESH_TOKEN_HEADER, JwtUtil.BEARER_PREFIX + refreshTokenHeader) - .when().post("/logout") - .then().statusCode(HttpStatus.SC_OK) - .apply(document("logout", requestPreprocessor(), responsePreprocessor(), + .when() + .post("/logout") + .then() + .statusCode(org.apache.http.HttpStatus.SC_OK) + // ApiResponse.success()의 스키마에 맞춰 본문 필드 검증 (필요 시 조정) + .body("result", org.hamcrest.Matchers.equalTo("SUCCESS")) + .apply(document("logout", + requestPreprocessor(), + responsePreprocessor(), requestHeaders( headerWithName(HttpHeaders.AUTHORIZATION) .description("엑세스 토큰 (Bearer prefix 포함)"), headerWithName(JwtUtil.REFRESH_TOKEN_HEADER) - .description("리프레시 토큰 (Bearer prefix 포함)")), + .description("리프레시 토큰 (Bearer prefix 포함)") + ), responseFields( - fieldWithPath("result").type(JsonFieldType.STRING).description("요청 결과")))); + fieldWithPath("result").type(JsonFieldType.STRING).description("요청 결과") + ) + )); + + // revokeUserTokens가 단 한 번 호출되었는지 확인 + then(tokenService).should(org.mockito.Mockito.times(1)) + .revokeUserTokens(anyString(), anyString()); } + } diff --git a/capturecat-core/src/test/java/com/capturecat/core/api/error/ErrorCodeControllerTest.java b/capturecat-core/src/test/java/com/capturecat/core/api/error/ErrorCodeControllerTest.java index cd9c777..b8ec280 100644 --- a/capturecat-core/src/test/java/com/capturecat/core/api/error/ErrorCodeControllerTest.java +++ b/capturecat-core/src/test/java/com/capturecat/core/api/error/ErrorCodeControllerTest.java @@ -82,8 +82,8 @@ void setUp() { @Test void 로그아웃_에러_코드_문서() { - List errorCodeDescriptors = generateErrorCodeDescriptors(INVALID_REFRESH_TOKEN, - REFRESH_TOKEN_EXPIRED); + List errorCodeDescriptors = generateErrorCodeDescriptors(INVALID_LOGOUT_AUTH_TOKEN, + INTERNAL_SERVER_ERROR); generateErrorDocs("errorCode/logout", errorCodeDescriptors); } @@ -142,7 +142,7 @@ void setUp() { @Test void 회원탈퇴_에러_코드_문서() { List errorCodeDescriptors = generateErrorCodeDescriptors(USER_NOT_FOUND, - UNLINK_SOCIAL_FAIL); + UNLINK_SOCIAL_FAIL, INVALID_ACCESS_TOKEN, INVALID_REFRESH_TOKEN, INTERNAL_SERVER_ERROR); generateErrorDocs("errorCode/withdraw", errorCodeDescriptors); } diff --git a/capturecat-core/src/test/java/com/capturecat/core/api/user/UserControllerTest.java b/capturecat-core/src/test/java/com/capturecat/core/api/user/UserControllerTest.java index 265e88c..177ff8a 100644 --- a/capturecat-core/src/test/java/com/capturecat/core/api/user/UserControllerTest.java +++ b/capturecat-core/src/test/java/com/capturecat/core/api/user/UserControllerTest.java @@ -16,7 +16,6 @@ import io.restassured.http.ContentType; -import com.capturecat.core.api.user.dto.UserReqDto; import com.capturecat.core.api.user.dto.UserReqDto.WithdrawReqDto; import com.capturecat.core.api.user.dto.UserRespDto; import com.capturecat.core.config.jwt.JwtUtil; @@ -30,6 +29,7 @@ class UserControllerTest extends RestDocsTest { private static final String URL_PREFIX = "/v1/user"; private static final String ACCESS_TOKEN = "valid-access-token"; + private static final String REFRESH_TOKEN = "valid-refresh-token"; private final ObjectMapper om = new ObjectMapper(); @@ -76,6 +76,7 @@ void setUp() { // when & then given() .header(HttpHeaders.AUTHORIZATION, JwtUtil.BEARER_PREFIX + ACCESS_TOKEN) + .header(JwtUtil.REFRESH_TOKEN_HEADER, JwtUtil.BEARER_PREFIX + REFRESH_TOKEN) .contentType(ContentType.JSON) .body(requestBody) .when() @@ -86,7 +87,9 @@ void setUp() { .apply(document("withdraw", requestPreprocessor(), responsePreprocessor(), requestHeaders( headerWithName(HttpHeaders.AUTHORIZATION) - .description("유효한 Access 토큰")), + .description("유효한 Access 토큰"), + headerWithName(JwtUtil.REFRESH_TOKEN_HEADER) + .description("유효한 Refresh 토큰")), requestFields( fieldWithPath("reason").type(JsonFieldType.STRING).description("탈퇴 사유")), responseFields( diff --git a/capturecat-core/src/test/java/com/capturecat/core/config/jwt/JwtLogoutFilterTest.java b/capturecat-core/src/test/java/com/capturecat/core/config/jwt/JwtLogoutFilterTest.java index 90c3df1..f30eb62 100644 --- a/capturecat-core/src/test/java/com/capturecat/core/config/jwt/JwtLogoutFilterTest.java +++ b/capturecat-core/src/test/java/com/capturecat/core/config/jwt/JwtLogoutFilterTest.java @@ -3,6 +3,11 @@ import static com.capturecat.core.config.jwt.JwtUtil.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.*; +import static org.mockito.BDDMockito.any; +import static org.mockito.BDDMockito.anyString; +import static org.mockito.BDDMockito.doThrow; +import static org.mockito.BDDMockito.verify; +import static org.mockito.Mockito.*; import jakarta.servlet.FilterChain; @@ -27,8 +32,10 @@ class JwtLogoutFilterTest { @Mock private TokenService tokenService; + @InjectMocks private JwtLogoutFilter jwtLogoutFilter; + @Mock private FilterChain filterChain; @@ -42,22 +49,25 @@ void init() { } @Test - @DisplayName("/logout 요청이 아닐 경우 무시") + @DisplayName("/logout 요청이 아니면 체인을 그대로 통과") void non_logout_request() throws Exception { - //given + // given + request.setMethod("GET"); request.setRequestURI("/not-logout"); - //when + // when jwtLogoutFilter.doFilter(request, response, filterChain); - //then + // then verify(filterChain).doFilter(request, response); + verify(tokenService, never()).revokeUserTokens(anyString(), anyString()); } @Test - @DisplayName("리프레시 토큰이 없는 경우 UNAUTHORIZED 응답") + @DisplayName("POST /logout 이지만 토큰 헤더가 없으면 401") void logout_request_without_token() throws Exception { // given + request.setMethod("POST"); request.setRequestURI("/logout"); // when @@ -65,16 +75,17 @@ void logout_request_without_token() throws Exception { // then assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatus()); - verify(tokenService, never()).deleteValidRefreshToken(anyString()); + verify(tokenService, never()).revokeUserTokens(anyString(), anyString()); verify(filterChain, never()).doFilter(any(), any()); } @Test - @DisplayName("토큰 검증 도중 예외 발생 시 INVALID_REFRESH_TOKEN 에러 발생") + @DisplayName("POST /logout 중 revokeUserTokens 에서 예외 발생 시 401") void logout_request_blacklist_exception() throws Exception { // given + request.setMethod("POST"); request.setRequestURI("/logout"); - String accessToken = "invalid-refresh-token"; + String accessToken = "invalid-access-token"; String refreshToken = "invalid-refresh-token"; request.addHeader(HttpHeaders.AUTHORIZATION, BEARER_PREFIX + accessToken); request.addHeader(REFRESH_TOKEN_HEADER, BEARER_PREFIX + refreshToken); @@ -92,17 +103,16 @@ void logout_request_blacklist_exception() throws Exception { } @Test - @DisplayName("유효한 리프레시 토큰이 있는 경우 정상 처리") + @DisplayName("POST /logout + 유효한 토큰 헤더면 200") void logout_request_with_valid_token() throws Exception { // given + request.setMethod("POST"); request.setRequestURI("/logout"); - String accessToken = "valid-refresh-token"; + String accessToken = "valid-access-token"; String refreshToken = "valid-refresh-token"; request.addHeader(HttpHeaders.AUTHORIZATION, BEARER_PREFIX + accessToken); request.addHeader(REFRESH_TOKEN_HEADER, BEARER_PREFIX + refreshToken); - - // 토큰 서비스 mock: 항상 정상 처리 willDoNothing().given(tokenService).revokeUserTokens(anyString(), anyString()); // when