diff --git a/apps/user-service/build.gradle b/apps/user-service/build.gradle index e49ac98b..46ce0961 100644 --- a/apps/user-service/build.gradle +++ b/apps/user-service/build.gradle @@ -8,7 +8,7 @@ plugins { } group = 'site.icebang' -version = '0.0.1-beta-stable' +version = '0.0.1-beta-STABLE' description = 'Ice bang - fast campus team4' java { diff --git a/apps/user-service/src/main/java/site/icebang/global/config/security/SecurityConfig.java b/apps/user-service/src/main/java/site/icebang/global/config/security/SecurityConfig.java index 3543a8dd..514998e2 100644 --- a/apps/user-service/src/main/java/site/icebang/global/config/security/SecurityConfig.java +++ b/apps/user-service/src/main/java/site/icebang/global/config/security/SecurityConfig.java @@ -82,6 +82,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .permitAll() .requestMatchers("/v0/auth/check-session") .authenticated() + .requestMatchers(SecurityEndpoints.SUPER_ADMIN.getMatchers()) + .hasAnyRole("SUPER_ADMIN") .requestMatchers(SecurityEndpoints.DATA_ADMIN.getMatchers()) .hasRole("SUPER_ADMIN") // hasAuthority -> hasRole .requestMatchers(SecurityEndpoints.DATA_ENGINEER.getMatchers()) diff --git a/apps/user-service/src/main/java/site/icebang/global/config/security/endpoints/SecurityEndpoints.java b/apps/user-service/src/main/java/site/icebang/global/config/security/endpoints/SecurityEndpoints.java index e6e24243..98129d8b 100644 --- a/apps/user-service/src/main/java/site/icebang/global/config/security/endpoints/SecurityEndpoints.java +++ b/apps/user-service/src/main/java/site/icebang/global/config/security/endpoints/SecurityEndpoints.java @@ -11,7 +11,6 @@ public enum SecurityEndpoints { "/js/**", "/images/**", "/v0/organizations/**", - "/v0/auth/register", "/v0/check-execution-log-insert"), // 데이터 관리 관련 엔드포인트 @@ -27,7 +26,9 @@ public enum SecurityEndpoints { OPS("/api/scheduler/**", "/api/monitoring/**"), // 일반 사용자 엔드포인트 - USER("/user/**", "/profile/**", "/v0/auth/check-session", "/v0/workflows/**"); + USER("/user/**", "/profile/**", "/v0/auth/check-session", "/v0/workflows/**"), + + SUPER_ADMIN("/v0/auth/register"); private final String[] patterns; diff --git a/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java b/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java index 8243acde..0711cf90 100644 --- a/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java +++ b/apps/user-service/src/main/java/site/icebang/global/handler/exception/GlobalExceptionHandler.java @@ -1,8 +1,11 @@ package site.icebang.global.handler.exception; +import java.util.stream.Collectors; + import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.AuthenticationException; +import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; @@ -19,9 +22,13 @@ public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) - public ApiResponse handleValidation(MethodArgumentNotValidException ex) { - String detail = ex.getBindingResult().toString(); - return ApiResponse.error("Validation failed: " + detail, HttpStatus.BAD_REQUEST); + public ApiResponse handleValidation(MethodArgumentNotValidException ex) { + String errorMessage = + ex.getBindingResult().getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + + return ApiResponse.error("입력 값 검증 실패: " + errorMessage, HttpStatus.BAD_REQUEST); } @ExceptionHandler(Exception.class) diff --git a/apps/user-service/src/test/java/site/icebang/integration/tests/auth/AuthApiIntegrationTest.java b/apps/user-service/src/test/java/site/icebang/integration/tests/auth/AuthApiIntegrationTest.java index 4fe3b00d..95d0cfbd 100644 --- a/apps/user-service/src/test/java/site/icebang/integration/tests/auth/AuthApiIntegrationTest.java +++ b/apps/user-service/src/test/java/site/icebang/integration/tests/auth/AuthApiIntegrationTest.java @@ -7,6 +7,7 @@ import static org.springframework.restdocs.payload.PayloadDocumentation.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -15,6 +16,8 @@ import org.springframework.http.*; import org.springframework.mock.web.MockHttpSession; import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.test.context.support.WithUserDetails; import org.springframework.test.context.jdbc.Sql; import org.springframework.transaction.annotation.Transactional; @@ -29,7 +32,7 @@ class AuthApiIntegrationTest extends IntegrationTestSupport { @Test @DisplayName("사용자 로그인 성공") - void login_success() throws Exception { + void loginSuccess() throws Exception { // given Map loginRequest = new HashMap<>(); loginRequest.put("email", "admin@icebang.site"); @@ -81,9 +84,243 @@ void login_success() throws Exception { .build()))); } + @Test + @DisplayName("사용자 등록 실패 - 이메일 양식 오류") + @WithUserDetails("admin@icebang.site") + void registerFailureWhenInvalidEmail() throws Exception { + // given + Map registerRequest = new HashMap<>(); + registerRequest.put("name", "홍길동"); + registerRequest.put("email", "invalid-email"); // 잘못된 이메일 형식 + registerRequest.put("orgId", 1); + registerRequest.put("deptId", 1); + registerRequest.put("positionId", 1); + registerRequest.put("roleIds", Arrays.asList(1)); + + // when & then + mockMvc + .perform( + post(getApiUrlForDocs("/v0/auth/register")) + .contentType(MediaType.APPLICATION_JSON) + .header("Origin", "https://admin.icebang.site") + .header("Referer", "https://admin.icebang.site/") + .content(objectMapper.writeValueAsString(registerRequest))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value("BAD_REQUEST")) + .andDo( + document( + "auth-register-invalid-email", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Authentication") + .summary("사용자 회원가입 실패 - 잘못된 이메일") + .description("잘못된 이메일 형식으로 인한 회원가입 실패") + .requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("사용자명"), + fieldWithPath("email") + .type(JsonFieldType.STRING) + .description("잘못된 형식의 이메일 주소"), + fieldWithPath("orgId").type(JsonFieldType.NUMBER).description("조직 ID"), + fieldWithPath("deptId").type(JsonFieldType.NUMBER).description("부서 ID"), + fieldWithPath("positionId") + .type(JsonFieldType.NUMBER) + .description("직책 ID"), + fieldWithPath("roleIds") + .type(JsonFieldType.ARRAY) + .description("역할 ID 목록")) + .responseFields( + fieldWithPath("success") + .type(JsonFieldType.BOOLEAN) + .description("요청 성공 여부"), + fieldWithPath("data").type(JsonFieldType.NULL).description("에러 시 null"), + fieldWithPath("message") + .type(JsonFieldType.STRING) + .description("에러 메시지"), + fieldWithPath("status") + .type(JsonFieldType.STRING) + .description("HTTP 상태")) + .build()))); + } + + @Test + @DisplayName("사용자 등록 실패 - 필수 필드 누락") + @WithUserDetails("admin@icebang.site") + void registerFailureWhenMissingRequiredFields() throws Exception { + // given - 필수 필드 누락 + Map registerRequest = new HashMap<>(); + registerRequest.put("email", "test@icebang.site"); + // name, orgId, deptId, positionId, roleIds 누락 + + // when & then + mockMvc + .perform( + post(getApiUrlForDocs("/v0/auth/register")) + .contentType(MediaType.APPLICATION_JSON) + .header("Origin", "https://admin.icebang.site") + .header("Referer", "https://admin.icebang.site/") + .content(objectMapper.writeValueAsString(registerRequest))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value("BAD_REQUEST")) + .andDo( + document( + "auth-register-missing-fields", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Authentication") + .summary("사용자 회원가입 실패 - 필수 필드 누락") + .description("필수 필드 누락으로 인한 회원가입 실패") + .requestFields( + fieldWithPath("email") + .type(JsonFieldType.STRING) + .description("사용자 이메일 주소")) + .responseFields( + fieldWithPath("success") + .type(JsonFieldType.BOOLEAN) + .description("요청 성공 여부"), + fieldWithPath("data").type(JsonFieldType.NULL).description("에러 시 null"), + fieldWithPath("message") + .type(JsonFieldType.STRING) + .description("에러 메시지"), + fieldWithPath("status") + .type(JsonFieldType.STRING) + .description("HTTP 상태")) + .build()))); + } + + @Test + @DisplayName("사용자 등록 실패 - Authentication이 없는 경우") + void registerFailureWhenAuthenticationMissing() throws Exception { + // given + Map registerRequest = new HashMap<>(); + registerRequest.put("name", "홍길동"); + registerRequest.put("email", "hong@icebang.site"); + registerRequest.put("orgId", 1); + registerRequest.put("deptId", 1); + registerRequest.put("positionId", 1); + registerRequest.put("roleIds", Arrays.asList(1, 2)); + + // when & then + mockMvc + .perform( + post(getApiUrlForDocs("/v0/auth/register")) + .contentType(MediaType.APPLICATION_JSON) + .header("Origin", "https://admin.icebang.site") + .header("Referer", "https://admin.icebang.site/") + .content(objectMapper.writeValueAsString(registerRequest))) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value("UNAUTHORIZED")) + .andExpect(jsonPath("$.message").value("Authentication required")) + .andExpect(jsonPath("$.data").isEmpty()) + .andDo( + document( + "auth-register-unauthorized", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Authentication") + .summary("사용자 회원가입 실패 - 인증 없음") + .description("인증 정보가 없어서 회원가입 실패") + .requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("사용자명"), + fieldWithPath("email") + .type(JsonFieldType.STRING) + .description("사용자 이메일 주소"), + fieldWithPath("orgId").type(JsonFieldType.NUMBER).description("조직 ID"), + fieldWithPath("deptId").type(JsonFieldType.NUMBER).description("부서 ID"), + fieldWithPath("positionId") + .type(JsonFieldType.NUMBER) + .description("직책 ID"), + fieldWithPath("roleIds") + .type(JsonFieldType.ARRAY) + .description("역할 ID 목록")) + .responseFields( + fieldWithPath("success") + .type(JsonFieldType.BOOLEAN) + .description("요청 성공 여부"), + fieldWithPath("data").type(JsonFieldType.NULL).description("에러 시 null"), + fieldWithPath("message") + .type(JsonFieldType.STRING) + .description("인증 에러 메시지"), + fieldWithPath("status") + .type(JsonFieldType.STRING) + .description("HTTP 상태")) + .build()))); + } + + @Test + @DisplayName("사용자 등록 실패 - Permission이 없는 경우") + @WithMockUser("content.choi@icebang.site") + void registerFailureWhenNoPermissionProvided() throws Exception { + // given + Map registerRequest = new HashMap<>(); + registerRequest.put("name", "홍길동"); + registerRequest.put("email", "hong@icebang.site"); + registerRequest.put("orgId", 1); + registerRequest.put("deptId", 1); + registerRequest.put("positionId", 1); + registerRequest.put("roleIds", Arrays.asList(1, 2)); + + // when & then + mockMvc + .perform( + post(getApiUrlForDocs("/v0/auth/register")) + .contentType(MediaType.APPLICATION_JSON) + .header("Origin", "https://admin.icebang.site") + .header("Referer", "https://admin.icebang.site/") + .content(objectMapper.writeValueAsString(registerRequest))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value("FORBIDDEN")) + .andExpect(jsonPath("$.message").value("Access denied")) + .andExpect(jsonPath("$.data").isEmpty()) + .andDo( + document( + "auth-register-forbidden", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource( + ResourceSnippetParameters.builder() + .tag("Authentication") + .summary("사용자 회원가입 실패 - 권한 부족") + .description("적절한 권한이 없어서 회원가입 실패") + .requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("사용자명"), + fieldWithPath("email") + .type(JsonFieldType.STRING) + .description("사용자 이메일 주소"), + fieldWithPath("orgId").type(JsonFieldType.NUMBER).description("조직 ID"), + fieldWithPath("deptId").type(JsonFieldType.NUMBER).description("부서 ID"), + fieldWithPath("positionId") + .type(JsonFieldType.NUMBER) + .description("직책 ID"), + fieldWithPath("roleIds") + .type(JsonFieldType.ARRAY) + .description("역할 ID 목록")) + .responseFields( + fieldWithPath("success") + .type(JsonFieldType.BOOLEAN) + .description("요청 성공 여부"), + fieldWithPath("data").type(JsonFieldType.NULL).description("에러 시 null"), + fieldWithPath("message") + .type(JsonFieldType.STRING) + .description("권한 에러 메시지"), + fieldWithPath("status") + .type(JsonFieldType.STRING) + .description("HTTP 상태")) + .build()))); + } + @Test @DisplayName("사용자 로그아웃 성공") - void logout_success() throws Exception { + void logoutSuccess() throws Exception { // given - 먼저 로그인 Map loginRequest = new HashMap<>(); loginRequest.put("email", "admin@icebang.site");