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
Binary file added gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
7 changes: 7 additions & 0 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
4 changes: 2 additions & 2 deletions gradlew

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

94 changes: 94 additions & 0 deletions gradlew.bat

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 8 additions & 8 deletions src/main/java/com/issueDive/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand Down Expand Up @@ -49,18 +50,17 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS 설정
// 9월1일 변경 - 세션 관리 정책 (JWT는 stateless)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize -> authorize
// 모든 요청(/**)을 허용 (임시로)
//.anyRequest().permitAll() // 개발 단계에서는 전체 허용
.requestMatchers(PUBLIC_URLS).permitAll() // 공개 URL은 모두 허용
.requestMatchers("/auth/**", "/login").permitAll() // 1. 로그인/인증 관련 경로는 모두 허용
.requestMatchers(HttpMethod.GET, "/issues", "/issues/**").permitAll() // 2. 이슈 조회(GET)는 모두 허용
.requestMatchers(HttpMethod.GET, "/labels", "/labels/**").permitAll() // 3. 라벨 조회(GET)도 모두 허용
.anyRequest().authenticated() // 나머지는 인증 필요
)
.formLogin(formLogin -> formLogin.disable()) // 폼 로그인 비활성화 (필요 시)
.logout(logout -> logout.disable());
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);// 로그아웃 비활성화 (필요 시)
.formLogin(formLogin -> formLogin.disable()) // 폼 로그인 비활성화 (서버 사이드 렌더링 사용X)
.logout(logout -> logout.disable()); // 로그아웃 비활성화 (서버에 로그인 상태 저장X: Stateless)
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}

Expand Down
8 changes: 7 additions & 1 deletion src/main/java/com/issueDive/service/IssueService.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,13 @@ public Page<IssueResponse> getFilteredIssues(IssueFilterRequest filter) {
if (filter.labelIds()!=null && !filter.labelIds().isEmpty()) builder.and(qIssue.labels.any().id.in(filter.labelIds()));

if (filter.query() != null && !filter.query().isBlank()) {
builder.and(qIssue.title.containsIgnoreCase(filter.query()));
String searchQuery = filter.query();
builder.and(
qIssue.title.containsIgnoreCase(searchQuery) // 1. 제목에서 검색
.or(qIssue.author.username.containsIgnoreCase(searchQuery)) // 2. 작성자 이름으로 검색
.or(qIssue.assignee.username.containsIgnoreCase(searchQuery)) // 3. 담당자 이름으로 검색
.or(qIssue.labels.any().name.containsIgnoreCase(searchQuery)) // 4. 라벨 이름 중 하나라도 일치하면 검색
);
}

// 페이징 객체
Expand Down
68 changes: 50 additions & 18 deletions src/test/java/com/issueDive/config/SecurityConfigTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.issueDive.security.CustomUserDetailsService;
import com.issueDive.security.JwtAuthenticationFilter;
import com.issueDive.service.IssueService;
import com.issueDive.util.JwtUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
Expand All @@ -20,6 +21,7 @@

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.doNothing;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
Expand All @@ -36,6 +38,8 @@ public class SecurityConfigTest {

@MockitoBean
private CustomUserDetailsService customUserDetailsService;
@MockitoBean
private IssueService issueService;

private static final String VALID_TOKEN = "valid.jwt.token";
private static final String USER_EMAIL = "test@example.com";
Expand Down Expand Up @@ -81,26 +85,43 @@ void publicUrl_Swagger_AllowedWithoutAuth() throws Exception {
}

@Test
@DisplayName("보호된 URL 인증 없이 접근 차단")
void protectedUrl_WithoutAuth_Blocked() throws Exception {
@DisplayName("permitAll()된 GET /issues는 인증 없이 접근 가능")
void publicGetIssues_WithoutAuth_Allowed() throws Exception {
// GET /issues는 이제 permitAll이므로 403 Forbidden이 아닌 200 OK를 기대해야 합니다.
mockMvc.perform(get("/issues"))
.andDo(print())
.andExpect(status().isForbidden());
.andExpect(status().isOk());
}

@Test
@DisplayName("보호된 URL(POST /issues)은 인증 없이 접근 차단")
void protectedPostUrl_WithoutAuth_Blocked() throws Exception {
// 테스트 대상을 GET이 아닌 POST로 변경하여 보호 여부를 확인합니다.
// POST, PATCH, DELETE 등은 여전히 인증이 필요합니다.
mockMvc.perform(post("/issues")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\":\"Test\"}"))
.andDo(print())
.andExpect(status().isForbidden()); // JWT 필터가 없으므로 403 Forbidden
}

@Test
@DisplayName("유효한 JWT 토큰으로 보호된 URL 접근 허용")
@DisplayName("유효한 JWT 토큰으로 보호된 URL(POST /issues) 접근 허용")
void protectedUrl_WithValidToken_Allowed() throws Exception {
// given
given(jwtUtil.getUserEmailFromToken(VALID_TOKEN)).willReturn(USER_EMAIL);
given(jwtUtil.validateToken(VALID_TOKEN, USER_EMAIL)).willReturn(true);
given(customUserDetailsService.loadUserByUsername(USER_EMAIL)).willReturn(userDetails);

// when & then
mockMvc.perform(get("/issues")
.header("Authorization", "Bearer " + VALID_TOKEN))
String validIssueJson = "{\"title\":\"Test Issue\",\"description\":\"Test Description\"}";

// 테스트 대상을 GET이 아닌 POST로 변경하여 토큰 인증을 테스트합니다.
mockMvc.perform(post("/issues")
.header("Authorization", "Bearer " + VALID_TOKEN)
.contentType(MediaType.APPLICATION_JSON)
.content(validIssueJson))
.andDo(print())
.andExpect(status().isOk()); // 또는 404 (컨트롤러 없음)
.andExpect(status().isCreated()); // 컨트롤러 로직에 따라 201 Created 또는 다른 성공 코드를 기대
}

@Test
Expand All @@ -111,11 +132,13 @@ void protectedUrl_WithInvalidToken_Blocked() throws Exception {
given(jwtUtil.getUserEmailFromToken(invalidToken)).willReturn(USER_EMAIL);
given(jwtUtil.validateToken(invalidToken, USER_EMAIL)).willReturn(false);

// when & then
mockMvc.perform(get("/issues")
.header("Authorization", "Bearer " + invalidToken))
// 테스트 대상을 GET이 아닌 POST로 변경
mockMvc.perform(post("/issues")
.header("Authorization", "Bearer " + invalidToken)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\":\"Test\"}"))
.andDo(print())
.andExpect(status().isUnauthorized());
.andExpect(status().isUnauthorized()); // JwtAuthenticationFilter에서 401 Unauthorized 반환
}

@Test
Expand Down Expand Up @@ -156,14 +179,17 @@ void sessionPolicy_Stateless() throws Exception {
// 첫 번째 요청
mockMvc.perform(post("/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.content("{\"email\":\"test@test.com\", \"password\":\"password\"}"))
.andDo(print())
.andExpect(status().is4xxClientError()); // 400 Bad Request
.andExpect(status().isOk());

// 두 번째 요청 - 세션이 유지되지 않아야 함
mockMvc.perform(get("/issues"))
// 두 번째 요청도 GET /issues로 보내면 permitAll 이므로 성공합니다.
// 대신 보호된 경로인 POST /issues로 보내서 세션이 유지되지 않음을 확인합니다.
mockMvc.perform(post("/issues")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\":\"Test\"}"))
.andDo(print())
.andExpect(status().isForbidden());
.andExpect(status().isForbidden()); // 세션이 없으므로 인증 실패
}

@Test
Expand Down Expand Up @@ -199,14 +225,20 @@ void httpMethod_POST_Allowed() throws Exception {
@Test
@DisplayName("HTTP 메서드별 접근 제어 - DELETE")
void httpMethod_DELETE_Allowed() throws Exception {
// given: 인증 관련 설정 (기존과 동일)
given(jwtUtil.getUserEmailFromToken(VALID_TOKEN)).willReturn(USER_EMAIL);
given(jwtUtil.validateToken(VALID_TOKEN, USER_EMAIL)).willReturn(true);
given(customUserDetailsService.loadUserByUsername(USER_EMAIL)).willReturn(userDetails);

// given: IssueService의 deleteIssue 메소드가 호출될 때 아무것도 하지 않도록 설정 (성공 시나리오)
doNothing().when(issueService).deleteIssue(1L);

// when & then
mockMvc.perform(delete("/issues/1")
.header("Authorization", "Bearer " + VALID_TOKEN))
.andDo(print())
.andExpect(status().is4xxClientError()); // 404 (리소스 없음)
// then: 4xx 에러가 아닌, 성공 상태 코드인 200 OK를 기대하도록 변경
.andExpect(status().isOk());
}

@Test
Expand Down
11 changes: 1 addition & 10 deletions src/test/java/com/issueDive/controller/AuthControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
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;
Expand Down Expand Up @@ -57,15 +58,6 @@ public class AuthControllerTest {
@MockitoBean
private CustomUserDetailsService customUserDetailsService;

@MockitoBean
private JwtUtil jwtUtil;

@MockitoBean // 9월 2일 변경: AuthenticationManager mock 추가
private AuthenticationManager authenticationManager;

@MockitoBean // 9월 2일 수정: CustomUserDetailsService Mock 추가 (빈 찾을 수 없음 에러 해결)
private CustomUserDetailsService customUserDetailsService;

@Test
@DisplayName("[SUCCESS] POST /auth/signup - 회원가입 성공")
void signUp_success() throws Exception {
Expand Down Expand Up @@ -228,4 +220,3 @@ void logout_success() throws Exception {
}

}
}