package kr.itn.itnhub.org; import kr.itn.itnhub.AbstractDbTest; import kr.itn.itnhub.mattermost.MattermostClient; import kr.itn.itnhub.mattermost.MattermostException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.nullValue; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @AutoConfigureMockMvc @WithMockUser(roles = "ADMIN") class OrganizationControllerTest extends AbstractDbTest { @Autowired MockMvc mvc; @Autowired OrganizationMapper mapper; @Autowired JdbcTemplate jdbc; @MockBean MattermostClient mattermost; private Long orgId; @BeforeEach void setUp() { jdbc.update("delete from organization"); Organization org = new Organization(); org.setOrgNo("001"); org.setOrgName("국제방송교류재단"); org.setChannelSlug("001"); mapper.upsertBySeed(org); orgId = mapper.findAll().get(0).getId(); } @Test void 기관_목록을_상태와_함께_돌려준다() throws Exception { mvc.perform(get("/api/orgs")) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].orgNo").value("001")) .andExpect(jsonPath("$[0].orgName").value("국제방송교류재단")) .andExpect(jsonPath("$[0].status").value("INFO_PENDING")); } @Test void 담당자정보를_저장하면_생성가능_상태가_된다() throws Exception { mvc.perform(put("/api/orgs/{id}/contact", orgId) .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(""" { "deptName": "데이터정보화팀", "managerName": "송민지", "managerTitle": "과장", "managerPhone": "02-3475-5434", "managerEmail": "ming@arirang.com" } """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value("READY")); assertThat(mapper.findById(orgId).getDeptName()).isEqualTo("데이터정보화팀"); } @Test void 문정원_담당자와_변호사_정보를_포함해_저장하면_응답과_재조회에_모두_반영된다() throws Exception { mvc.perform(put("/api/orgs/{id}/contact", orgId) .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(""" { "deptName": "데이터정보화팀", "managerName": "송민지", "managerTitle": "과장", "managerPhone": "02-3475-5434", "managerEmail": "ming@arirang.com", "mjDeptName": "문화체육관광부 저작권정책과", "mjManagerName": "김문정", "mjManagerPhone": "02-1234-5678", "mjManagerEmail": "mj@mcst.go.kr", "lawyerName": "이변호", "lawyerPhone": "02-9876-5432", "lawyerEmail": "lawyer@lawfirm.kr", "lawyerAssignedDate": "2026-07-21" } """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.mjDeptName").value("문화체육관광부 저작권정책과")) .andExpect(jsonPath("$.mjManagerName").value("김문정")) .andExpect(jsonPath("$.mjManagerPhone").value("02-1234-5678")) .andExpect(jsonPath("$.mjManagerEmail").value("mj@mcst.go.kr")) .andExpect(jsonPath("$.lawyerName").value("이변호")) .andExpect(jsonPath("$.lawyerPhone").value("02-9876-5432")) .andExpect(jsonPath("$.lawyerEmail").value("lawyer@lawfirm.kr")) .andExpect(jsonPath("$.lawyerAssignedDate").value("2026-07-21")); mvc.perform(get("/api/orgs")) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].mjManagerName").value("김문정")) .andExpect(jsonPath("$[0].lawyerName").value("이변호")); } @Test void 문정원_변호사_정보없이_기존방식_페이로드만으로도_저장에_성공한다() throws Exception { mvc.perform(put("/api/orgs/{id}/contact", orgId) .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(""" { "deptName": "데이터정보화팀", "managerName": "송민지", "managerTitle": "과장", "managerPhone": "02-3475-5434", "managerEmail": "ming@arirang.com" } """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value("READY")) .andExpect(jsonPath("$.mjManagerName").value(nullValue())) .andExpect(jsonPath("$.lawyerName").value(nullValue())); } @Test void 존재하지_않는_기관ID로_담당자정보를_수정하면_404다() throws Exception { long missingId = orgId + 999999L; mvc.perform(put("/api/orgs/{id}/contact", missingId) .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(""" { "deptName": "데이터정보화팀", "managerName": "송민지", "managerTitle": "과장", "managerPhone": "02-3475-5434", "managerEmail": "ming@arirang.com" } """)) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.message").value(containsString(String.valueOf(missingId)))); } @Test void 필수값이_비면_400이다() throws Exception { mvc.perform(put("/api/orgs/{id}/contact", orgId) .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(""" { "deptName": "", "managerName": "송민지", "managerPhone": "02-3475-5434", "managerEmail": "ming@arirang.com" } """)) .andExpect(status().isBadRequest()); } /** * Finding I6 회귀 테스트: 검증 실패 응답 본문이 필드명을 그대로 노출하는 원시 JSON * 블롭(예: {"deptName":"must not be blank"})이 아니라, 운영자가 읽을 수 있는 * 한글 라벨(부서명 등)이 담긴 {@link kr.itn.itnhub.config.ApiError} 메시지여야 한다. */ @Test void 필수값이_비면_한글_필드명이_담긴_메시지를_돌려준다() throws Exception { mvc.perform(put("/api/orgs/{id}/contact", orgId) .with(csrf()) .contentType(MediaType.APPLICATION_JSON) .content(""" { "deptName": "", "managerName": "", "managerPhone": "02-3475-5434", "managerEmail": "ming@arirang.com" } """)) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value(containsString("부서명"))) .andExpect(jsonPath("$.message").value(containsString("담당자명"))); } @Test void 채널생성을_요청하면_결과를_돌려준다() throws Exception { Organization ready = mapper.findById(orgId); ready.setDeptName("데이터정보화팀"); ready.setManagerName("송민지"); ready.setManagerTitle("과장"); ready.setManagerPhone("02-3475-5434"); ready.setManagerEmail("ming@arirang.com"); mapper.updateContact(ready); when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty()); when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty()); when(mattermost.createPrivateChannel(anyString(), anyString())).thenReturn("chan"); mvc.perform(post("/api/orgs/{id}/channels", orgId).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.mj").value("CREATED")) .andExpect(jsonPath("$.law").value("CREATED")); } @Test void 담당자정보없이_채널생성하면_실패결과를_돌려준다() throws Exception { mvc.perform(post("/api/orgs/{id}/channels", orgId).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.mj").value("FAILED")) .andExpect(jsonPath("$.law").value("FAILED")) .andExpect(jsonPath("$.message").value(containsString("담당자 정보"))); } /** * Finding 2 회귀 테스트: 문정원 채널은 성공하고 법률검토 채널만 실패하는 실제 혼합 * 결과를 컨트롤러 경계에서 검증한다. 기존 테스트는 전부 성공 아니면(둘 다 성공) * 담당자 정보 누락으로 둘 다 강제 FAILED인 경로만 다뤄, 진짜 부분 실패가 HTTP * 200으로 내려가는지는 한 번도 실행되지 않았다. */ @Test void 법률검토만_실패해도_200과_부분실패_결과를_돌려준다() throws Exception { Organization ready = mapper.findById(orgId); ready.setDeptName("데이터정보화팀"); ready.setManagerName("송민지"); ready.setManagerTitle("과장"); ready.setManagerPhone("02-3475-5434"); ready.setManagerEmail("ming@arirang.com"); mapper.updateContact(ready); when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty()); when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty()); when(mattermost.createPrivateChannel(eq("org-001-mj"), anyString())).thenReturn("id-mj"); when(mattermost.createPrivateChannel(eq("org-001-law"), anyString())) .thenThrow(new MattermostException("법률검토 서버 오류")); mvc.perform(post("/api/orgs/{id}/channels", orgId).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.mj").value("CREATED")) .andExpect(jsonPath("$.law").value("FAILED")) .andExpect(jsonPath("$.message").value(containsString("법률검토"))); } }