Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions src/main/java/com/issueDive/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,7 +21,6 @@

import java.util.Arrays;

import com.issueDive.security.CustomUserDetailsService;
import com.issueDive.security.JwtAuthenticationFilter;

@Configuration
Expand Down Expand Up @@ -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();
}
Expand Down
3 changes: 1 addition & 2 deletions src/main/java/com/issueDive/controller/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,7 +23,7 @@

@Tag(name = "Auth & User", description = "인증 및 사용자 관리 API")
@RestController
@RequestMapping("/auth")
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class
AuthController {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down
5 changes: 1 addition & 4 deletions src/main/java/com/issueDive/controller/IssueController.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -25,7 +22,7 @@

@Tag(name = "Issue", description = "이슈 관리 API")
@RestController
@RequestMapping("/issues")
@RequestMapping("/api/issues")
@RequiredArgsConstructor
public class IssueController {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

@Tag(name = "Label", description = "레이블 관리 API")
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class LabelController {
private final LabelService labelService;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.ArrayList;

@Component
@RequiredArgsConstructor
Expand Down Expand Up @@ -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()));
}

/**
Expand All @@ -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");
Expand Down
5 changes: 4 additions & 1 deletion src/test/java/com/issueDive/IssueDiveApplicationTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ class IssueDiveApplicationTests {
@Autowired
private IssueAssigneeRepository issueAssigneeRepository;


private static final String API_PREFIX = "/api";

@BeforeEach
void setUp() {
// 각 테스트 실행 전에 데이터베이스를 비움
Expand Down Expand Up @@ -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())
Expand Down
25 changes: 14 additions & 11 deletions src/test/java/com/issueDive/config/SecurityConfigTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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())
Expand All @@ -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())
Expand All @@ -93,15 +96,15 @@ 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());
}

@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());
}
Expand All @@ -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())
Expand All @@ -121,23 +124,23 @@ 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());
}

@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());
}

@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());
}
Expand All @@ -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))
Expand All @@ -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\"}"))
Expand Down Expand Up @@ -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())
Expand Down
5 changes: 4 additions & 1 deletion src/test/java/com/issueDive/config/XssFilterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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)))
Expand Down
21 changes: 11 additions & 10 deletions src/test/java/com/issueDive/controller/AuthControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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를 통해
Expand Down Expand Up @@ -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 {
Expand All @@ -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 상태 코드 확인
Expand All @@ -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 상태 코드 확인
Expand All @@ -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 상태 코드 확인
Expand All @@ -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"));
Expand All @@ -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 상태 코드 확인
}

Expand All @@ -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"))
Expand All @@ -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());
}

Expand All @@ -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())
Expand All @@ -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을 제거하세요."));
Expand Down
Loading
Loading