File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
package kr.itn.itnhub.feed;
import kr.itn.itnhub.AbstractDbTest;
import kr.itn.itnhub.mattermost.MattermostClient;
import kr.itn.itnhub.org.Organization;
import kr.itn.itnhub.org.OrganizationMapper;
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.HttpHeaders;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
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.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
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 ChannelFeedControllerTest 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();
mapper.updateChannelIdMj(orgId, "chan-mj");
mapper.updateChannelIdLaw(orgId, "chan-law");
}
@Test
void 게시물_조회는_문정원_채널로_라우팅한다() throws Exception {
when(mattermost.getRecentPosts(eq("chan-mj"), eq(60))).thenReturn(List.of(
new PostView("p1", "송민지", "안녕하세요", 100L, false, List.of())));
mvc.perform(get("/api/orgs/{id}/posts", orgId).param("channel", "mj"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].id").value("p1"))
.andExpect(jsonPath("$[0].user").value("송민지"))
.andExpect(jsonPath("$[0].message").value("안녕하세요"));
}
@Test
void 게시물_조회는_law_파라미터면_법률검토_채널로_라우팅한다() throws Exception {
when(mattermost.getRecentPosts(eq("chan-law"), eq(60))).thenReturn(List.of(
new PostView("p2", "변호사", "검토합니다", 200L, false, List.of())));
mvc.perform(get("/api/orgs/{id}/posts", orgId).param("channel", "law"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].id").value("p2"));
}
@Test
void 자료_조회는_문정원_채널로_라우팅한다() throws Exception {
when(mattermost.collectChannelFiles(eq("chan-mj"))).thenReturn(List.of(
new FileView("f1", "보고서.pdf", 1234L, "application/pdf", 100L, "송민지")));
mvc.perform(get("/api/orgs/{id}/files", orgId).param("channel", "mj"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].id").value("f1"))
.andExpect(jsonPath("$[0].name").value("보고서.pdf"))
.andExpect(jsonPath("$[0].uploader").value("송민지"));
}
@Test
void 채널이_아직_생성되지_않았으면_409다() throws Exception {
jdbc.update("delete from organization");
Organization org = new Organization();
org.setOrgNo("002");
org.setOrgName("생성전기관");
org.setChannelSlug("002");
mapper.upsertBySeed(org);
Long freshOrgId = mapper.findAll().get(0).getId();
mvc.perform(get("/api/orgs/{id}/posts", freshOrgId).param("channel", "mj"))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.message").value("해당 채널이 아직 생성되지 않았습니다."));
}
@Test
void 존재하지_않는_기관이면_404다() throws Exception {
long missingId = orgId + 999999L;
mvc.perform(get("/api/orgs/{id}/posts", missingId).param("channel", "mj"))
.andExpect(status().isNotFound());
}
@Test
void 알수없는_채널_구분이면_409다() throws Exception {
mvc.perform(get("/api/orgs/{id}/posts", orgId).param("channel", "etc"))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.message").value("알 수 없는 채널 구분입니다: etc"));
}
@Test
void 일반_메시지_전송은_게시글만_만들고_공지_처리를_하지_않는다() throws Exception {
when(mattermost.createPost(eq("chan-mj"), eq("안녕하세요"), eq(List.of())))
.thenReturn("post-1");
when(mattermost.getRecentPosts(eq("chan-mj"), eq(1))).thenReturn(List.of(
new PostView("post-1", "관리자", "안녕하세요", 100L, false, List.of())));
mvc.perform(multipart("/api/orgs/{id}/messages", orgId)
.param("channel", "mj")
.param("message", "안녕하세요")
.with(csrf()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.post.id").value("post-1"))
.andExpect(jsonPath("$.noticeWarning").isEmpty());
verify(mattermost, never()).pinPost(anyString());
verify(mattermost, never()).unpinPost(anyString());
verify(mattermost, never()).updateChannelHeader(anyString(), anyString());
}
@Test
void 공지_전송은_기존핀을_해제하고_새핀과_헤더를_설정한다() throws Exception {
when(mattermost.getPinnedPosts(eq("chan-mj"))).thenReturn(List.of(
new PostView("old-pin", "관리자", "이전 공지", 50L, false, List.of())));
when(mattermost.createPost(eq("chan-mj"), eq("새 공지"), eq(List.of())))
.thenReturn("post-2");
when(mattermost.getRecentPosts(eq("chan-mj"), eq(1))).thenReturn(List.of(
new PostView("post-2", "관리자", "새 공지", 200L, false, List.of())));
mvc.perform(multipart("/api/orgs/{id}/messages", orgId)
.param("channel", "mj")
.param("message", "새 공지")
.param("notice", "true")
.with(csrf()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.post.id").value("post-2"))
.andExpect(jsonPath("$.noticeWarning").isEmpty());
verify(mattermost).unpinPost(eq("old-pin"));
verify(mattermost).pinPost(eq("post-2"));
org.mockito.ArgumentCaptor<String> headerCaptor =
org.mockito.ArgumentCaptor.forClass(String.class);
verify(mattermost).updateChannelHeader(eq("chan-mj"), headerCaptor.capture());
assertThat(headerCaptor.getValue())
.startsWith("📢 공지: 새 공지")
.contains("/itn-hub/pl/post-2");
}
@Test
void 파일첨부_전송은_업로드한_파일id를_게시글에_싣는다() throws Exception {
when(mattermost.uploadFile(eq("chan-mj"), eq("계약서.pdf"), any(), eq("application/pdf")))
.thenReturn("file-1");
when(mattermost.createPost(eq("chan-mj"), eq(""), eq(List.of("file-1"))))
.thenReturn("post-3");
when(mattermost.getRecentPosts(eq("chan-mj"), eq(1))).thenReturn(List.of(
new PostView("post-3", "관리자", "", 300L, false,
List.of(new FileRef("file-1", "계약서.pdf", 4L, "application/pdf")))));
mvc.perform(multipart("/api/orgs/{id}/messages", orgId)
.file(new org.springframework.mock.web.MockMultipartFile(
"files", "계약서.pdf", "application/pdf", "data".getBytes()))
.param("channel", "mj")
.with(csrf()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.post.id").value("post-3"));
verify(mattermost).createPost(eq("chan-mj"), eq(""), eq(List.of("file-1")));
}
@Test
void 내용도_파일도_없으면_400이다() throws Exception {
mvc.perform(multipart("/api/orgs/{id}/messages", orgId)
.param("channel", "mj")
.with(csrf()))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message")
.value("메시지 내용이나 첨부파일 중 하나는 있어야 합니다."));
}
@Test
void 메시지_전송시_존재하지_않는_기관이면_404다() throws Exception {
mvc.perform(multipart("/api/orgs/{id}/messages", orgId + 999999L)
.param("channel", "mj")
.param("message", "안녕")
.with(csrf()))
.andExpect(status().isNotFound());
}
@Test
void 공지_조회는_고정글이_있으면_돌려준다() throws Exception {
when(mattermost.getPinnedPosts(eq("chan-mj"))).thenReturn(List.of(
new PostView("n1", "관리자", "공지문", 100L, false, List.of())));
mvc.perform(get("/api/orgs/{id}/notice", orgId).param("channel", "mj"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("공지문"));
}
@Test
void 공지_조회는_고정글이_없으면_204다() throws Exception {
when(mattermost.getPinnedPosts(eq("chan-mj"))).thenReturn(List.of());
mvc.perform(get("/api/orgs/{id}/notice", orgId).param("channel", "mj"))
.andExpect(status().isNoContent());
}
@Test
void 공지_해제는_핀을_전부_풀고_헤더를_비운다() throws Exception {
when(mattermost.getPinnedPosts(eq("chan-mj"))).thenReturn(List.of(
new PostView("n1", "관리자", "공지1", 100L, false, List.of()),
new PostView("n2", "관리자", "공지2", 200L, false, List.of())));
mvc.perform(delete("/api/orgs/{id}/notice", orgId)
.param("channel", "mj")
.with(csrf()))
.andExpect(status().isNoContent());
verify(mattermost).unpinPost(eq("n1"));
verify(mattermost).unpinPost(eq("n2"));
verify(mattermost).updateChannelHeader(eq("chan-mj"), eq(""));
}
@Test
void 파일_다운로드는_한글파일명을_UTF8로_인코딩한_Content_Disposition을_설정한다() throws Exception {
when(mattermost.fileInfo(eq("f1")))
.thenReturn(new FileRef("f1", "보고서.pdf", 1234L, "application/pdf"));
when(mattermost.downloadFile(eq("f1"))).thenReturn("hello".getBytes());
mvc.perform(get("/api/files/{fileId}", "f1"))
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename*=UTF-8''%EB%B3%B4%EA%B3%A0%EC%84%9C.pdf"));
}
}