diff --git a/src/main/java/com/issueDive/config/SecurityConfig.java b/src/main/java/com/issueDive/config/SecurityConfig.java index c135ec0..40c1461 100644 --- a/src/main/java/com/issueDive/config/SecurityConfig.java +++ b/src/main/java/com/issueDive/config/SecurityConfig.java @@ -9,6 +9,7 @@ import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @@ -20,7 +21,6 @@ import java.util.Arrays; -import com.issueDive.security.CustomUserDetailsService; import com.issueDive.security.JwtAuthenticationFilter; @Configuration @@ -49,13 +49,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authorize -> authorize .requestMatchers(PUBLIC_URLS).permitAll() // 공개 URL은 모두 허용 - .requestMatchers("/auth/signup", "/auth/login").permitAll() // 1. 로그인/인증 관련 경로는 모두 허용 - .requestMatchers(HttpMethod.GET, "/issues", "/issues/**").permitAll() // 2. 이슈 조회(GET)는 모두 허용 - .requestMatchers(HttpMethod.GET, "/labels", "/labels/**").permitAll() // 3. 라벨 조회(GET)도 모두 허용 + .requestMatchers("/api/auth/signup", "/api/auth/login").permitAll() // 1. 로그인/인증 관련 경로는 모두 허용 + .requestMatchers(HttpMethod.GET, "/api/issues", "/api/issues/**").permitAll() // 2. 이슈 조회(GET)는 모두 허용 + .requestMatchers(HttpMethod.GET, "/api/labels", "/api/labels/**").permitAll() // 3. 라벨 조회(GET)도 모두 허용 .anyRequest().authenticated() // 나머지는 인증 필요 ) - .formLogin(formLogin -> formLogin.disable()) // 폼 로그인 비활성화 (서버 사이드 렌더링 사용X) - .logout(logout -> logout.disable()); // 로그아웃 비활성화 (서버에 로그인 상태 저장X: Stateless) + .formLogin(AbstractHttpConfigurer::disable) // 폼 로그인 비활성화 (서버 사이드 렌더링 사용X) + .logout(AbstractHttpConfigurer::disable); // 로그아웃 비활성화 (서버에 로그인 상태 저장X: Stateless) http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } diff --git a/src/main/java/com/issueDive/controller/AuthController.java b/src/main/java/com/issueDive/controller/AuthController.java index fc91157..f0c6e0f 100644 --- a/src/main/java/com/issueDive/controller/AuthController.java +++ b/src/main/java/com/issueDive/controller/AuthController.java @@ -10,7 +10,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import com.issueDive.dto.*; -import com.issueDive.service.UserService; import jakarta.validation.Valid; import lombok.*; import org.springframework.http.HttpStatus; @@ -24,7 +23,7 @@ @Tag(name = "Auth & User", description = "인증 및 사용자 관리 API") @RestController -@RequestMapping("/auth") +@RequestMapping("/api/auth") @RequiredArgsConstructor public class AuthController { diff --git a/src/main/java/com/issueDive/controller/CommentController.java b/src/main/java/com/issueDive/controller/CommentController.java index cb2a8f1..8197ea7 100644 --- a/src/main/java/com/issueDive/controller/CommentController.java +++ b/src/main/java/com/issueDive/controller/CommentController.java @@ -1,9 +1,6 @@ package com.issueDive.controller; import com.issueDive.dto.*; -import com.issueDive.entity.User; -import com.issueDive.exception.UserNotFoundException; -import com.issueDive.repository.UserRepository; import com.issueDive.service.CommentService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -25,7 +22,7 @@ @Tag(name = "Comment", description = "댓글 관리 API") @RestController @RequiredArgsConstructor -@RequestMapping("/issues/{issueId}/comments") +@RequestMapping("/api/issues/{issueId}/comments") public class CommentController { private final CommentService commentService; private final UserService userService; diff --git a/src/main/java/com/issueDive/controller/IssueController.java b/src/main/java/com/issueDive/controller/IssueController.java index 86566c6..2efeb7f 100644 --- a/src/main/java/com/issueDive/controller/IssueController.java +++ b/src/main/java/com/issueDive/controller/IssueController.java @@ -1,9 +1,6 @@ package com.issueDive.controller; import com.issueDive.dto.*; -import com.issueDive.entity.User; -import com.issueDive.exception.UserNotFoundException; -import com.issueDive.repository.UserRepository; import com.issueDive.service.IssueService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -25,7 +22,7 @@ @Tag(name = "Issue", description = "이슈 관리 API") @RestController -@RequestMapping("/issues") +@RequestMapping("/api/issues") @RequiredArgsConstructor public class IssueController { diff --git a/src/main/java/com/issueDive/controller/LabelController.java b/src/main/java/com/issueDive/controller/LabelController.java index 9bbf415..506e7b7 100644 --- a/src/main/java/com/issueDive/controller/LabelController.java +++ b/src/main/java/com/issueDive/controller/LabelController.java @@ -24,6 +24,7 @@ @Tag(name = "Label", description = "레이블 관리 API") @RestController +@RequestMapping("/api") @RequiredArgsConstructor public class LabelController { private final LabelService labelService; diff --git a/src/main/java/com/issueDive/security/JwtAuthenticationFilter.java b/src/main/java/com/issueDive/security/JwtAuthenticationFilter.java index 3522df6..bf7eb72 100644 --- a/src/main/java/com/issueDive/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/issueDive/security/JwtAuthenticationFilter.java @@ -17,7 +17,6 @@ import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; -import java.util.ArrayList; @Component @RequiredArgsConstructor @@ -95,7 +94,7 @@ private void setErrorResponse(HttpServletResponse response, String message) thro }, "timestamp": "%s" } - """, message, java.time.LocalDateTime.now().toString())); + """, message, java.time.LocalDateTime.now())); } /** @@ -104,9 +103,9 @@ private void setErrorResponse(HttpServletResponse response, String message) thro @Override protected boolean shouldNotFilter(HttpServletRequest request) { String path = request.getRequestURI(); - return path.startsWith("/auth/signup") || - path.startsWith("/auth/login") || - path.startsWith("/auth/refresh") || + return path.startsWith("/api/auth/signup") || + path.startsWith("/api/auth/login") || + path.startsWith("/api/auth/refresh") || path.startsWith("/swagger-ui") || path.startsWith("/v3/api-docs")|| path.startsWith("/actuator"); diff --git a/src/test/java/com/issueDive/IssueDiveApplicationTests.java b/src/test/java/com/issueDive/IssueDiveApplicationTests.java index bc865db..a9fadff 100644 --- a/src/test/java/com/issueDive/IssueDiveApplicationTests.java +++ b/src/test/java/com/issueDive/IssueDiveApplicationTests.java @@ -42,6 +42,9 @@ class IssueDiveApplicationTests { @Autowired private IssueAssigneeRepository issueAssigneeRepository; + + private static final String API_PREFIX = "/api"; + @BeforeEach void setUp() { // 각 테스트 실행 전에 데이터베이스를 비움 @@ -78,7 +81,7 @@ void getFilteredIssues_integrationTest() throws Exception { .build()); // when: API를 호출해 필터링된 결과 요청 - mockMvc.perform(get("/issues") + mockMvc.perform(get(API_PREFIX + "/issues") .param("status", "OPEN") // OPEN 상태인 이슈만 필터링 .param("authorId", author.getId().toString()) .param("assigneeIds", assignee.getId().toString()) diff --git a/src/test/java/com/issueDive/config/SecurityConfigTest.java b/src/test/java/com/issueDive/config/SecurityConfigTest.java index 7316149..88f81c7 100644 --- a/src/test/java/com/issueDive/config/SecurityConfigTest.java +++ b/src/test/java/com/issueDive/config/SecurityConfigTest.java @@ -53,6 +53,9 @@ public class SecurityConfigTest { @MockitoBean private TokenBlacklistService tokenBlacklistService; + + private static final String API_PREFIX = "/api"; + private static final String VALID_TOKEN = "valid.jwt.token"; private static final String USER_EMAIL = "test@example.com"; private UserDetails userDetails; @@ -73,7 +76,7 @@ void setUp() { @Test @DisplayName("공개 URL - /auth/signup 인증 없이 접근 가능") void publicUrl_Signup_AllowedWithoutAuth() throws Exception { - mockMvc.perform(post("/auth/signup") + mockMvc.perform(post(API_PREFIX + "/auth/signup") .contentType(MediaType.APPLICATION_JSON) .content("{}")) .andDo(print()) @@ -83,7 +86,7 @@ void publicUrl_Signup_AllowedWithoutAuth() throws Exception { @Test @DisplayName("공개 URL - /auth/login 인증 없이 접근 가능") void publicUrl_Login_AllowedWithoutAuth() throws Exception { - mockMvc.perform(post("/auth/login") + mockMvc.perform(post(API_PREFIX + "/auth/login") .contentType(MediaType.APPLICATION_JSON) .content("{}")) .andDo(print()) @@ -93,7 +96,7 @@ void publicUrl_Login_AllowedWithoutAuth() throws Exception { @Test @DisplayName("공개 URL - GET /issues 인증 없이 접근 가능") void publicUrl_GetIssues_AllowedWithoutAuth() throws Exception { - mockMvc.perform(get("/issues")) + mockMvc.perform(get(API_PREFIX + "/issues")) .andDo(print()) .andExpect(status().isOk()); } @@ -101,7 +104,7 @@ void publicUrl_GetIssues_AllowedWithoutAuth() throws Exception { @Test @DisplayName("공개 URL - GET /labels 인증 없이 접근 가능") void publicUrl_GetLabels_AllowedWithoutAuth() throws Exception { - mockMvc.perform(get("/labels")) + mockMvc.perform(get(API_PREFIX + "/labels")) .andDo(print()) .andExpect(status().isOk()); } @@ -111,7 +114,7 @@ void publicUrl_GetLabels_AllowedWithoutAuth() throws Exception { @Test @DisplayName("보호된 URL - POST /issues 인증 없이 접근 차단") void protectedUrl_PostIssues_BlockedWithoutAuth() throws Exception { - mockMvc.perform(post("/issues") + mockMvc.perform(post(API_PREFIX + "/issues") .contentType(MediaType.APPLICATION_JSON) .content("{\"title\":\"Test\"}")) .andDo(print()) @@ -121,7 +124,7 @@ void protectedUrl_PostIssues_BlockedWithoutAuth() throws Exception { @Test @DisplayName("보호된 URL - DELETE /issues/{id} 인증 없이 접근 차단") void protectedUrl_DeleteIssue_BlockedWithoutAuth() throws Exception { - mockMvc.perform(delete("/issues/1")) + mockMvc.perform(delete(API_PREFIX + "/issues/1")) .andDo(print()) .andExpect(status().isForbidden()); } @@ -129,7 +132,7 @@ void protectedUrl_DeleteIssue_BlockedWithoutAuth() throws Exception { @Test @DisplayName("보호된 URL - /auth/logout 인증 없이 접근 차단") void protectedUrl_Logout_BlockedWithoutAuth() throws Exception { - mockMvc.perform(post("/auth/logout")) + mockMvc.perform(post(API_PREFIX + "/auth/logout")) .andDo(print()) .andExpect(status().isForbidden()); } @@ -137,7 +140,7 @@ void protectedUrl_Logout_BlockedWithoutAuth() throws Exception { @Test @DisplayName("보호된 URL - /auth/users/{id} 인증 없이 접근 차단") void protectedUrl_GetUser_BlockedWithoutAuth() throws Exception { - mockMvc.perform(get("/auth/users/1")) + mockMvc.perform(get(API_PREFIX + "/auth/users/1")) .andDo(print()) .andExpect(status().isForbidden()); } @@ -164,7 +167,7 @@ void protectedUrl_WithValidToken_Allowed() throws Exception { String validIssueJson = "{\"title\":\"Test Issue\",\"description\":\"Test Description\"}"; // when & then - mockMvc.perform(post("/issues") + mockMvc.perform(post(API_PREFIX + "/issues") .header("Authorization", "Bearer " + VALID_TOKEN) .contentType(MediaType.APPLICATION_JSON) .content(validIssueJson)) @@ -181,7 +184,7 @@ void protectedUrl_WithInvalidToken_Blocked() throws Exception { given(jwtUtil.validateToken(invalidToken, USER_EMAIL)).willReturn(false); // when & then - mockMvc.perform(post("/issues") + mockMvc.perform(post(API_PREFIX + "/issues") .header("Authorization", "Bearer " + invalidToken) .contentType(MediaType.APPLICATION_JSON) .content("{\"title\":\"Test\"}")) @@ -217,7 +220,7 @@ void cors_NotAllowedOrigin_Blocked() throws Exception { @Test @DisplayName("Stateless 세션 정책 - JSESSIONID 쿠키 생성 안함") void sessionPolicy_Stateless_NoSessionCreated() throws Exception { - mockMvc.perform(post("/issues") + mockMvc.perform(post(API_PREFIX + "/issues") .contentType(MediaType.APPLICATION_JSON) .content("{\"title\":\"Test\"}")) .andDo(print()) diff --git a/src/test/java/com/issueDive/config/XssFilterTest.java b/src/test/java/com/issueDive/config/XssFilterTest.java index 3d09790..b396dfe 100644 --- a/src/test/java/com/issueDive/config/XssFilterTest.java +++ b/src/test/java/com/issueDive/config/XssFilterTest.java @@ -54,6 +54,9 @@ class XssFilterTest { @MockitoBean private CustomUserDetailsService customUserDetailsService; + + private static final String API_PREFIX = "/api"; + @BeforeEach void setUp() { UserResponseDTO mockUser = new UserResponseDTO(1L, "testuser", "test@example.com"); @@ -131,7 +134,7 @@ private void performTestAndAssert(String maliciousInput, String expectedSanitize CreateIssueRequest requestDto = new CreateIssueRequest(maliciousInput, "description", null, null); // when - mockMvc.perform(post("/issues") + mockMvc.perform(post(API_PREFIX + "/issues") .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestDto))) diff --git a/src/test/java/com/issueDive/controller/AuthControllerTest.java b/src/test/java/com/issueDive/controller/AuthControllerTest.java index 372c748..321c442 100644 --- a/src/test/java/com/issueDive/controller/AuthControllerTest.java +++ b/src/test/java/com/issueDive/controller/AuthControllerTest.java @@ -27,7 +27,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -import static org.mockito.MockitoAnnotations.openMocks; /** * @WebMvcTest: 웹 계층(컨트롤러)에 대한 슬라이스 테스트를 진행합니다. * @AutoConfigureMockMvc: MockMvc를 자동으로 설정하며, addFilters = false를 통해 @@ -59,6 +58,8 @@ public class AuthControllerTest { @MockitoBean private CustomUserDetailsService customUserDetailsService; + private static final String API_PREFIX = "/api"; + @Test @DisplayName("[SUCCESS] POST /auth/signup - 회원가입 성공") void signUp_success() throws Exception { @@ -76,7 +77,7 @@ void signUp_success() throws Exception { given(jwtUtil.generateAccessToken(anyLong(), anyString())).willReturn(mockToken); // when & then: API를 호출하고 응답을 검증 - mvc.perform(post("/auth/signup") + mvc.perform(post(API_PREFIX + "/auth/signup") .contentType(MediaType.APPLICATION_JSON) .content(om.writeValueAsString(requestBody))) .andExpect(status().isCreated()) // 201 Created 상태 코드 확인 @@ -102,7 +103,7 @@ void login_success() throws Exception { given(jwtUtil.generateAccessToken(anyLong(), anyString())).willReturn(mockToken); // when & then - mvc.perform(post("/auth/login") + mvc.perform(post(API_PREFIX + "/auth/login") .contentType(MediaType.APPLICATION_JSON) .content(om.writeValueAsString(requestBody))) .andExpect(status().isOk()) // 200 OK 상태 코드 확인 @@ -124,7 +125,7 @@ void login_fail() throws Exception { given(authenticationManager.authenticate(any())).willThrow(new AuthenticationFailedException()); // when & then - mvc.perform(post("/auth/login") + mvc.perform(post(API_PREFIX + "/auth/login") .contentType(MediaType.APPLICATION_JSON) .content(om.writeValueAsString(requestBody))) .andExpect(status().isUnauthorized()); // 401 Unauthorized 상태 코드 확인 @@ -138,7 +139,7 @@ void getUserById_success() throws Exception { .willReturn(new UserResponseDTO(1L, "alice", "alice@test.com")); // when & then - mvc.perform(get("/auth/users/{id}", 1L)) + mvc.perform(get(API_PREFIX + "/auth/users/{id}", 1L)) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.id").value(1L)) .andExpect(jsonPath("$.data.username").value("alice")); @@ -152,7 +153,7 @@ void getUserById_notFound() throws Exception { .willThrow(new UserNotFoundException(999L)); // when & then - mvc.perform(get("/auth/users/{id}", 999L)) + mvc.perform(get(API_PREFIX + "/auth/users/{id}", 999L)) .andExpect(status().isNotFound()); // 404 Not Found 상태 코드 확인 } @@ -167,7 +168,7 @@ void getAllUsers_success() throws Exception { given(userService.getAllUsers()).willReturn(userList); // when & then - mvc.perform(get("/auth/users")) + mvc.perform(get(API_PREFIX + "/auth/users")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data").isArray()) .andExpect(jsonPath("$.data[0].email").value("a@test.com")) @@ -181,7 +182,7 @@ void deleteUser_success() throws Exception { doNothing().when(userService).deleteUser(1L); // when & then - mvc.perform(delete("/auth/user/{id}", 1L)) + mvc.perform(delete(API_PREFIX + "/auth/user/{id}", 1L)) .andExpect(status().isOk()); } @@ -201,7 +202,7 @@ void login_withJWT_tokenGeneration() throws Exception { given(userService.findUserByEmail("alice@test.com")).willReturn(userResponse); given(jwtUtil.generateAccessToken(1L, "alice@test.com")).willReturn(mockToken); - mvc.perform(post("/auth/login") + mvc.perform(post(API_PREFIX + "/auth/login") .contentType(MediaType.APPLICATION_JSON) .content(om.writeValueAsString(req))) .andDo(print()) @@ -217,7 +218,7 @@ void login_withJWT_tokenGeneration() throws Exception { @Test @DisplayName("POST /auth/logout - 로그아웃 응답 확인") void logout_success() throws Exception { - mvc.perform(post("/auth/logout")) + mvc.perform(post(API_PREFIX + "/auth/logout")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.message").value("로그아웃되었습니다. 클라이언트에서 토큰을 삭제해주세요.")) .andExpect(jsonPath("$.data.instruction").value("localStorage에서 accessToken을 제거하세요.")); diff --git a/src/test/java/com/issueDive/controller/CommentControllerTest.java b/src/test/java/com/issueDive/controller/CommentControllerTest.java index c4a8ee4..ccf67ab 100644 --- a/src/test/java/com/issueDive/controller/CommentControllerTest.java +++ b/src/test/java/com/issueDive/controller/CommentControllerTest.java @@ -68,6 +68,8 @@ class CommentControllerTest { @MockitoBean private CustomUserDetailsService customUserDetailsService; + private static final String API_PREFIX = "/api"; + // 테스트에서 공통으로 사용할 ID 값들 private final Long issueId = 1L; private final Long commentId = 1L; @@ -97,7 +99,7 @@ void getComments_Success() throws Exception { when(commentService.getTreeByIssue(issueId)).thenReturn(Collections.emptyList()); // when & then: API 호출 및 응답 검증 - mockMvc.perform(get("/issues/{issueId}/comments", issueId)) + mockMvc.perform(get(API_PREFIX + "/issues/{issueId}/comments", issueId)) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)); @@ -115,7 +117,7 @@ void createComment_Success() throws Exception { when(commentService.createComment(eq(issueId), any(CreateCommentRequest.class), eq(currentUserId))).thenReturn(response); // when & then - mockMvc.perform(post("/issues/{issueId}/comments", issueId) + mockMvc.perform(post(API_PREFIX + "/issues/{issueId}/comments", issueId) .with(csrf()) // POST, PATCH, DELETE 등 CSRF 보호가 필요한 요청에 추가 .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) @@ -136,7 +138,7 @@ void updateComment_Success() throws Exception { when(commentService.updateComment(eq(issueId), eq(commentId), any(UpdateCommentRequest.class), eq(currentUserId))).thenReturn(response); // when & then - mockMvc.perform(patch("/issues/{issueId}/comments/{commentId}", issueId, commentId) + mockMvc.perform(patch(API_PREFIX + "/issues/{issueId}/comments/{commentId}", issueId, commentId) .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) @@ -153,7 +155,7 @@ void deleteComment_Success() throws Exception { doNothing().when(commentService).deleteComment(issueId, commentId, currentUserId); // when & then - mockMvc.perform(delete("/issues/{issueId}/comments/{commentId}", issueId, commentId) + mockMvc.perform(delete(API_PREFIX + "/issues/{issueId}/comments/{commentId}", issueId, commentId) .with(csrf())) .andDo(print()) .andExpect(status().isNoContent()); // 204 No Content 상태 코드 확인 @@ -170,7 +172,7 @@ void createComment_BlankDescription_ReturnsBadRequest() throws Exception { request.setDescription(""); // when & then - mockMvc.perform(post("/issues/{issueId}/comments", issueId) + mockMvc.perform(post(API_PREFIX + "/issues/{issueId}/comments", issueId) .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) @@ -189,7 +191,7 @@ void updateComment_CommentNotFound_ReturnsNotFound() throws Exception { .thenThrow(new CommentNotFoundException("댓글을 찾을 수 없습니다.")); // when & then - mockMvc.perform(patch("/issues/{issueId}/comments/{commentId}", issueId, commentId) + mockMvc.perform(patch(API_PREFIX + "/issues/{issueId}/comments/{commentId}", issueId, commentId) .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) @@ -206,7 +208,7 @@ void deleteComment_NotOwner_ReturnsForbidden() throws Exception { .when(commentService).deleteComment(issueId, commentId, currentUserId); // when & then - mockMvc.perform(delete("/issues/{issueId}/comments/{commentId}", issueId, commentId) + mockMvc.perform(delete(API_PREFIX + "/issues/{issueId}/comments/{commentId}", issueId, commentId) .with(csrf())) .andDo(print()) .andExpect(status().isForbidden()) // 403 Forbidden 상태 코드 확인 @@ -221,7 +223,7 @@ void countComment_Success() throws Exception { when(commentService.countByIssue(issueId)).thenReturn(new CountCommentResponse(issueId, count)); // when & then - mockMvc.perform(get("/issues/{issueId}/comments/count", issueId)) + mockMvc.perform(get(API_PREFIX + "/issues/{issueId}/comments/count", issueId)) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.issueId").value(issueId)) diff --git a/src/test/java/com/issueDive/controller/IssueControllerTest.java b/src/test/java/com/issueDive/controller/IssueControllerTest.java index 807a645..451a2cf 100644 --- a/src/test/java/com/issueDive/controller/IssueControllerTest.java +++ b/src/test/java/com/issueDive/controller/IssueControllerTest.java @@ -52,9 +52,6 @@ public class IssueControllerTest { @Autowired private MockMvc mockMvc; // HTTP 요청 시뮬레이션 객체 - @Autowired - private ObjectMapper objectMapper; // JSON <-> Java Object 변환 객체 - // --- MockitoBean: 테스트 대상 컨트롤러의 의존성을 가짜(Mock) 객체로 주입 --- @MockitoBean private IssueService issueService; @@ -70,6 +67,8 @@ public class IssueControllerTest { @MockitoBean private CustomUserDetailsService customUserDetailsService; + private static final String API_PREFIX = "/api"; + // 테스트 전역에서 사용할 Mock 유저의 ID private final Long currentUserId = 1L; @@ -111,7 +110,7 @@ public void createIssue_success() throws Exception { """; // when & then: API를 호출하고 응답을 검증 - mockMvc.perform(post("/issues") + mockMvc.perform(post(API_PREFIX + "/issues") .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) @@ -141,7 +140,7 @@ public void createIssue_withLabels_success() throws Exception { } """; - mockMvc.perform(post("/issues") + mockMvc.perform(post(API_PREFIX + "/issues") .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) @@ -165,7 +164,7 @@ void getIssues_returnsPagedResults() throws Exception { .thenReturn(mockPage); // when & then - mockMvc.perform(get("/issues") + mockMvc.perform(get(API_PREFIX + "/issues") .param("status", "OPEN") .param("page", "0")) .andExpect(status().isOk()) @@ -182,7 +181,7 @@ public void getIssue_success() throws Exception { Mockito.when(issueService.getIssue(1L)).thenReturn(mockResponse); // when & then - mockMvc.perform(get("/issues/1")) + mockMvc.perform(get(API_PREFIX + "/issues/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.data.id").value(1)) @@ -196,7 +195,7 @@ void getIssue_notFound_fail() throws Exception { Mockito.when(issueService.getIssue(999L)).thenThrow(new NotFoundException("Issue not found")); // when & then - mockMvc.perform(get("/issues/999")) + mockMvc.perform(get(API_PREFIX + "/issues/999")) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.error.code").value("IssueNotFound")); } @@ -215,7 +214,7 @@ void changeIssueStatus_success() throws Exception { """; // when & then - mockMvc.perform(patch("/issues/1/status") + mockMvc.perform(patch(API_PREFIX + "/issues/1/status") .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) @@ -238,7 +237,7 @@ void changeIssueStatus_invalidStatus_badRequest() throws Exception { """; // when & then - mockMvc.perform(patch("/issues/1/status") + mockMvc.perform(patch(API_PREFIX + "/issues/1/status") .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) diff --git a/src/test/java/com/issueDive/controller/LabelControllerTest.java b/src/test/java/com/issueDive/controller/LabelControllerTest.java index 08f4e0c..06720bf 100644 --- a/src/test/java/com/issueDive/controller/LabelControllerTest.java +++ b/src/test/java/com/issueDive/controller/LabelControllerTest.java @@ -41,9 +41,6 @@ class LabelControllerTest { @Autowired private MockMvc mockMvc; - @Autowired - private ObjectMapper objectMapper; // ObjectMapper 주입 - // --- MockitoBean: 테스트 대상 컨트롤러의 의존성을 가짜(Mock) 객체로 주입 --- @MockitoBean private LabelService labelService; @@ -58,6 +55,8 @@ class LabelControllerTest { @MockitoBean private CustomUserDetailsService customUserDetailsService; + private static final String API_PREFIX = "/api"; + @Test @DisplayName("[SUCCESS] POST /labels - 라벨 생성 성공") void createLabel_success() throws Exception { @@ -76,7 +75,7 @@ void createLabel_success() throws Exception { """; // when & then: API 호출 및 응답 검증 - mockMvc.perform(post("/labels") + mockMvc.perform(post(API_PREFIX + "/labels") .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) @@ -99,7 +98,7 @@ void createLabel_duplicateName_BadRequest() throws Exception { .thenThrow(new ValidationException(ErrorCode.DuplicateLabel, "이미 존재하는 라벨 이름입니다.")); // when & then - mockMvc.perform(post("/labels") + mockMvc.perform(post(API_PREFIX + "/labels") .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) @@ -118,7 +117,7 @@ void getLabels_success() throws Exception { Mockito.when(labelService.getLabels()).thenReturn(mockList); // when & then - mockMvc.perform(get("/labels")) + mockMvc.perform(get(API_PREFIX + "/labels")) .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.data").isArray()) @@ -138,7 +137,7 @@ void getLabel_success() throws Exception { Mockito.when(labelService.getLabel(10L)).thenReturn(mockResponse); // when & then - mockMvc.perform(get("/labels/10")) + mockMvc.perform(get(API_PREFIX + "/labels/10")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.id").value(10)) .andExpect(jsonPath("$.data.name").value("bug")) @@ -152,7 +151,7 @@ void getLabel_labelNotFound_notFound() throws Exception { Mockito.when(labelService.getLabel(99L)).thenThrow(new LabelNotFoundException("라벨을 찾을 수 없습니다.")); // when & then - mockMvc.perform(get("/labels/99")) + mockMvc.perform(get(API_PREFIX + "/labels/99")) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.error.code").value("LabelNotFound")); } @@ -169,7 +168,7 @@ void updateLabel_success() throws Exception { Mockito.when(labelService.updateLabel(eq(labelId), any())).thenReturn(mockResponse); // when & then - mockMvc.perform(patch("/labels/{labelId}", labelId) + mockMvc.perform(patch(API_PREFIX + "/labels/{labelId}", labelId) .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) @@ -186,7 +185,7 @@ void deleteLabel_success() throws Exception { Mockito.doNothing().when(labelService).deleteLabel(labelId); // when & then - mockMvc.perform(delete("/labels/{id}", labelId) + mockMvc.perform(delete(API_PREFIX + "/labels/{id}", labelId) .with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.message").value("Label 10 deleted successfully")); @@ -198,7 +197,7 @@ void deleteLabel_noMapping_success() throws Exception { Long labelId = 20L; Mockito.doNothing().when(labelService).deleteLabel(labelId); - mockMvc.perform(delete("/labels/{id}", labelId) + mockMvc.perform(delete(API_PREFIX + "/labels/{id}", labelId) .with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.message").value("Label 20 deleted successfully")); @@ -220,7 +219,7 @@ void addLabelsToIssue_success() throws Exception { String requestBody = "[10, 20]"; // when & then - mockMvc.perform(post("/issues/{issueId}/labels", issueId) + mockMvc.perform(post(API_PREFIX + "/issues/{issueId}/labels", issueId) .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(requestBody)) @@ -245,7 +244,7 @@ void deleteLabelFromIssue_success() throws Exception { Mockito.when(issueLabelService.deleteLabelFromIssue(issueId, labelId)).thenReturn(mockResponse); // when & then - mockMvc.perform(delete("/issues/{issueId}/labels/{labelId}", issueId, labelId) + mockMvc.perform(delete(API_PREFIX + "/issues/{issueId}/labels/{labelId}", issueId, labelId) .with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.id").value(issueId)) diff --git a/src/test/java/com/issueDive/exception/GlobalExceptionHandlerTest.java b/src/test/java/com/issueDive/exception/GlobalExceptionHandlerTest.java index 3ef3c6a..7e6ad01 100644 --- a/src/test/java/com/issueDive/exception/GlobalExceptionHandlerTest.java +++ b/src/test/java/com/issueDive/exception/GlobalExceptionHandlerTest.java @@ -38,6 +38,8 @@ public class GlobalExceptionHandlerTest { @MockitoBean private CustomUserDetailsService customUserDetailsService; + + private static final String API_PREFIX = "/api"; // DuplicateEmail -> 409 @Test @@ -53,7 +55,7 @@ public void signUp_duplicateEmail_conflict() throws Exception { } """; - mockMvc.perform(post("/auth/signup") + mockMvc.perform(post(API_PREFIX + "/auth/signup") .contentType(MediaType.APPLICATION_JSON) .content(body)) .andExpect(status().isConflict()) @@ -76,7 +78,7 @@ public void login_authenticationFailed_unauthorized() throws Exception { """; // when & then: API 호출 시 401 Unauthorized 응답을 기대 - mockMvc.perform(post("/auth/login") + mockMvc.perform(post(API_PREFIX + "/auth/login") .contentType(MediaType.APPLICATION_JSON) .content(body)) .andExpect(status().isUnauthorized()) diff --git a/src/test/java/com/issueDive/security/JwtAuthenticationFilterTest.java b/src/test/java/com/issueDive/security/JwtAuthenticationFilterTest.java index 1f00bc2..33691b5 100644 --- a/src/test/java/com/issueDive/security/JwtAuthenticationFilterTest.java +++ b/src/test/java/com/issueDive/security/JwtAuthenticationFilterTest.java @@ -17,8 +17,6 @@ import org.springframework.security.core.userdetails.UserDetails; import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; import java.util.ArrayList; import static org.assertj.core.api.Assertions.assertThat; @@ -160,7 +158,7 @@ void doFilterInternal_ExceptionThrown_ShouldPass() throws ServletException, IOEx @DisplayName("공개 URL은 필터를 적용하지 않음 - /auth/signup") void shouldNotFilter_PublicUrl_Signup() { // given - given(request.getRequestURI()).willReturn("/auth/signup"); + given(request.getRequestURI()).willReturn("/api/auth/signup"); // when boolean shouldNotFilter = jwtAuthenticationFilter.shouldNotFilter(request); @@ -173,7 +171,7 @@ void shouldNotFilter_PublicUrl_Signup() { @DisplayName("공개 URL은 필터를 적용하지 않음 - /auth/login") void shouldNotFilter_PublicUrl_Login() { // given - given(request.getRequestURI()).willReturn("/auth/login"); + given(request.getRequestURI()).willReturn("/api/auth/login"); // when boolean shouldNotFilter = jwtAuthenticationFilter.shouldNotFilter(request);