package kr.re.etri.autoflow.controllers; import com.fasterxml.jackson.databind.ObjectMapper; import kr.re.etri.autoflow.entity.ProjectEntity; import kr.re.etri.autoflow.payload.request.ProjectRequest; import kr.re.etri.autoflow.service.ProjectService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.MediaType; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.web.SecurityFilterChain; import org.springframework.test.web.servlet.MockMvc; import java.time.LocalDate; import java.util.List; import java.util.Optional; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import org.springframework.beans.factory.annotation.Autowired; @WebMvcTest(ProjectController.class) @Import(ProjectControllerTest.MockConfig.class) class ProjectControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ProjectService projectService; @Autowired private ObjectMapper objectMapper; @TestConfiguration static class MockConfig { @Bean public ProjectService projectService() { return Mockito.mock(ProjectService.class); } // 테스트 시 Security 완전 무시 @Bean public SecurityFilterChain testSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz.anyRequest().permitAll()) .csrf(csrf -> csrf.disable()); return http.build(); } } private ProjectEntity sampleProject(Long id, String code, String name) { return ProjectEntity.builder() .id(id) .prjCd(code) .prjNm(name) .prjStartDt(LocalDate.of(2025, 8, 1)) .prjEndDt(LocalDate.of(2025, 12, 31)) .build(); } @Test @DisplayName("모든 프로젝트 조회") void getAllProjects() throws Exception { ProjectEntity entity = sampleProject(1L, "PRJ001", "AI 프로젝트"); when(projectService.findAll()).thenReturn(List.of(entity)); mockMvc.perform(get("/api/projects")) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].prjCd").value("PRJ001")); } @Nested class GetProjectByIdTests { @Test @DisplayName("ID로 프로젝트 조회 - 성공") void success() throws Exception { ProjectEntity entity = sampleProject(1L, "PRJ001", "AI 프로젝트"); when(projectService.findById(1L)).thenReturn(Optional.of(entity)); mockMvc.perform(get("/api/projects/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.prjCd").value("PRJ001")); } @Test @DisplayName("ID로 프로젝트 조회 - 실패") void notFound() throws Exception { when(projectService.findById(999L)).thenReturn(Optional.empty()); mockMvc.perform(get("/api/projects/999")) .andExpect(status().isNotFound()); } } @Nested class CreateProjectTests { @Test @DisplayName("프로젝트 생성 - 성공") void success() throws Exception { ProjectEntity input = sampleProject(null, "PRJ002", "새 프로젝트"); when(projectService.create(any())).thenReturn(input); mockMvc.perform(post("/api/projects") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(input))) .andExpect(status().isOk()) .andExpect(jsonPath("$.prjCd").value("PRJ002")); } @Test @DisplayName("프로젝트 생성 - 실패(중복 등)") void failure() throws Exception { ProjectEntity input = ProjectEntity.builder().prjCd("PRJ_DUP").build(); when(projectService.create(any())).thenThrow(new IllegalArgumentException("중복된 프로젝트 코드입니다.")); mockMvc.perform(post("/api/projects") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(input))) .andExpect(status().isBadRequest()) .andExpect(content().string("중복된 프로젝트 코드입니다.")); } } @Nested class UpdateProjectTests { @Test @DisplayName("프로젝트 수정 - 성공") void success() throws Exception { ProjectRequest request = new ProjectRequest(); request.setPrjNm("수정된 이름"); ProjectEntity updated = sampleProject(1L, "PRJ001", "수정된 이름"); when(projectService.update(Mockito.eq(1L), any())).thenReturn(Optional.of(updated)); mockMvc.perform(put("/api/projects/1") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()) .andExpect(jsonPath("$.prjNm").value("수정된 이름")); } @Test @DisplayName("프로젝트 수정 - 실패") void notFound() throws Exception { ProjectRequest request = new ProjectRequest(); request.setPrjNm("수정된 이름"); when(projectService.update(Mockito.eq(999L), any())).thenReturn(Optional.empty()); mockMvc.perform(put("/api/projects/999") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isNotFound()); } } @Nested class DeleteProjectTests { @Test @DisplayName("프로젝트 삭제 - 성공") void success() throws Exception { when(projectService.delete(1L)).thenReturn(true); mockMvc.perform(delete("/api/projects/1")) .andExpect(status().isNoContent()); } @Test @DisplayName("프로젝트 삭제 - 실패") void notFound() throws Exception { when(projectService.delete(999L)).thenReturn(false); mockMvc.perform(delete("/api/projects/999")) .andExpect(status().isNotFound()); } } }