File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
07-22
package kr.itn.itnhub.mattermost;
import com.github.tomakehurst.wiremock.WireMockServer;
import kr.itn.itnhub.config.MattermostProperties;
import kr.itn.itnhub.feed.FileRef;
import kr.itn.itnhub.feed.FileView;
import kr.itn.itnhub.feed.PostView;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
import java.net.http.HttpClient;
import java.util.List;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class MattermostRestClientTest {
private WireMockServer server;
private MattermostRestClient client;
@BeforeEach
void setUp() {
server = new WireMockServer(options().dynamicPort());
server.start();
MattermostProperties props = new MattermostProperties(
"http://localhost:" + server.port(),
"test-token",
"team123",
"문정원",
"법률검토");
// 이 HTTP/1.1 고정은 테스트 하네스(WireMock/Jetty)만의 제약이다: JDK 21 HttpClient의
// 기본 h2c 업그레이드 협상이 WireMock 상대로 POST 요청에서 간헐적으로 RST_STREAM을
// 유발하기 때문에 여기서만 고정한다. 운영 환경은 HTTPS/ALPN으로 협상하므로 해당되지 않는다.
HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.build();
RestClient.Builder builder = RestClient.builder()
.requestFactory(new JdkClientHttpRequestFactory(httpClient));
client = new MattermostRestClient(props, builder);
}
@AfterEach
void tearDown() {
server.stop();
}
@Test
void 내부명으로_채널을_찾으면_id를_돌려준다() {
server.stubFor(get(urlEqualTo("/api/v4/teams/team123/channels/name/org-001-mj"))
.willReturn(okJson("{\"id\":\"chan001mj\",\"name\":\"org-001-mj\"}")));
assertThat(client.findChannelIdByInternalName("org-001-mj"))
.contains("chan001mj");
}
@Test
void 내부명으로_채널이_없으면_빈값이다() {
server.stubFor(get(urlEqualTo("/api/v4/teams/team123/channels/name/org-999-mj"))
.willReturn(aResponse().withStatus(404)));
assertThat(client.findChannelIdByInternalName("org-999-mj")).isEmpty();
}
@Test
void 토큰을_Authorization_헤더로_보낸다() {
server.stubFor(get(urlEqualTo("/api/v4/teams/team123/channels/name/org-001-mj"))
.willReturn(okJson("{\"id\":\"chan001mj\"}")));
client.findChannelIdByInternalName("org-001-mj");
server.verify(getRequestedFor(urlEqualTo("/api/v4/teams/team123/channels/name/org-001-mj"))
.withHeader("Authorization", equalTo("Bearer test-token")));
}
@Test
void 표시명으로_팀전체_채널목록에서_id를_찾는다() {
server.stubFor(get(urlPathEqualTo("/api/v4/teams/team123/channels"))
.withQueryParam("page", equalTo("0"))
.withQueryParam("per_page", equalTo("200"))
.willReturn(okJson("""
[
{"id":"aaa","display_name":"001_국제방송교류재단 (문정원)"},
{"id":"bbb","display_name":"001_국제방송교류재단 (법률검토)"}
]
""")));
assertThat(client.findChannelIdByDisplayName("001_국제방송교류재단 (법률검토)"))
.contains("bbb");
}
@Test
void 표시명이_첫페이지에_없으면_빈값이다() {
server.stubFor(get(urlPathEqualTo("/api/v4/teams/team123/channels"))
.withQueryParam("page", equalTo("0"))
.withQueryParam("per_page", equalTo("200"))
.willReturn(okJson("""
[
{"id":"aaa","display_name":"001_국제방송교류재단 (문정원)"}
]
""")));
assertThat(client.findChannelIdByDisplayName("002_없는기관 (문정원)")).isEmpty();
}
@Test
void 표시명_조회가_두번째_페이지에서_찾으면_id를_돌려준다() {
server.stubFor(get(urlPathEqualTo("/api/v4/teams/team123/channels"))
.withQueryParam("page", equalTo("0"))
.withQueryParam("per_page", equalTo("200"))
.willReturn(okJson(fullPageJson(200, "p0", null))));
server.stubFor(get(urlPathEqualTo("/api/v4/teams/team123/channels"))
.withQueryParam("page", equalTo("1"))
.withQueryParam("per_page", equalTo("200"))
.willReturn(okJson(fullPageJson(5, "p1", "004_찾는기관 (법률검토)"))));
assertThat(client.findChannelIdByDisplayName("004_찾는기관 (법률검토)"))
.contains("p1-match");
}
@Test
void 표시명_조회가_두_페이지를_모두_소진하면_빈값이다() {
server.stubFor(get(urlPathEqualTo("/api/v4/teams/team123/channels"))
.withQueryParam("page", equalTo("0"))
.withQueryParam("per_page", equalTo("200"))
.willReturn(okJson(fullPageJson(200, "p0", null))));
server.stubFor(get(urlPathEqualTo("/api/v4/teams/team123/channels"))
.withQueryParam("page", equalTo("1"))
.withQueryParam("per_page", equalTo("200"))
.willReturn(okJson(fullPageJson(10, "p1", null))));
assertThat(client.findChannelIdByDisplayName("005_존재하지않는기관 (법률검토)")).isEmpty();
}
/** 팀 채널 목록 한 페이지를 흉내낸 JSON을 만든다. matchDisplayName이 있으면 마지막에 추가한다. */
private static String fullPageJson(int fillerCount, String idPrefix, String matchDisplayName) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < fillerCount; i++) {
if (i > 0) {
sb.append(",");
}
sb.append("{\"id\":\"").append(idPrefix).append("-filler").append(i)
.append("\",\"display_name\":\"filler-").append(idPrefix).append("-").append(i)
.append("\"}");
}
if (matchDisplayName != null) {
if (fillerCount > 0) {
sb.append(",");
}
sb.append("{\"id\":\"").append(idPrefix).append("-match\",\"display_name\":\"")
.append(matchDisplayName).append("\"}");
}
sb.append("]");
return sb.toString();
}
@Test
void 비공개_채널을_생성하고_id를_돌려준다() {
server.stubFor(post(urlEqualTo("/api/v4/channels"))
.willReturn(okJson("{\"id\":\"newchan\"}")));
String id = client.createPrivateChannel("org-002-mj", "002_세종학당재단 (문정원)");
assertThat(id).isEqualTo("newchan");
server.verify(postRequestedFor(urlEqualTo("/api/v4/channels"))
.withRequestBody(matchingJsonPath("$.team_id", equalTo("team123")))
.withRequestBody(matchingJsonPath("$.name", equalTo("org-002-mj")))
.withRequestBody(matchingJsonPath("$.display_name",
equalTo("002_세종학당재단 (문정원)")))
.withRequestBody(matchingJsonPath("$.type", equalTo("P"))));
}
@Test
void 서버가_오류를_주면_MattermostException을_던진다() {
server.stubFor(post(urlEqualTo("/api/v4/channels"))
.willReturn(aResponse().withStatus(500).withBody("boom")));
assertThatThrownBy(() -> client.createPrivateChannel("org-003-mj", "003_기관 (문정원)"))
.isInstanceOf(MattermostException.class);
}
@Test
void 게시글_조회는_최신순_응답을_오래된순으로_뒤집어_돌려준다() {
server.stubFor(get(urlPathEqualTo("/api/v4/channels/chan001mj/posts"))
.withQueryParam("per_page", equalTo("60"))
.willReturn(okJson("""
{
"order": ["p2", "p1"],
"posts": {
"p1": {"id":"p1","user_id":"u1","message":"먼저 씀","create_at":100,"type":""},
"p2": {"id":"p2","user_id":"u1","message":"나중에 씀","create_at":200,"type":""}
}
}
""")));
server.stubFor(get(urlPathEqualTo("/api/v4/users/u1"))
.willReturn(okJson("{\"username\":\"user1\",\"nickname\":\"\"}")));
List<PostView> posts = client.getRecentPosts("chan001mj", 60);
assertThat(posts).extracting(PostView::id).containsExactly("p1", "p2");
assertThat(posts).extracting(PostView::message).containsExactly("먼저 씀", "나중에 씀");
}
@Test
void 시스템_메시지는_system_플래그가_참이다() {
server.stubFor(get(urlPathEqualTo("/api/v4/channels/chan001mj/posts"))
.withQueryParam("per_page", equalTo("60"))
.willReturn(okJson("""
{
"order": ["p1"],
"posts": {
"p1": {"id":"p1","user_id":"u1","message":"님이 입장했습니다.","create_at":100,"type":"system_join_channel"}
}
}
""")));
server.stubFor(get(urlPathEqualTo("/api/v4/users/u1"))
.willReturn(okJson("{\"username\":\"user1\"}")));
List<PostView> posts = client.getRecentPosts("chan001mj", 60);
assertThat(posts).hasSize(1);
assertThat(posts.get(0).system()).isTrue();
}
@Test
void 게시글의_첨부파일_메타데이터를_읽는다() {
server.stubFor(get(urlPathEqualTo("/api/v4/channels/chan001mj/posts"))
.withQueryParam("per_page", equalTo("60"))
.willReturn(okJson("""
{
"order": ["p1"],
"posts": {
"p1": {
"id":"p1","user_id":"u1","message":"자료 첨부","create_at":100,"type":"",
"metadata": {
"files": [
{"id":"f1","name":"보고서.pdf","size":1234,"mime_type":"application/pdf"}
]
}
}
}
}
""")));
server.stubFor(get(urlPathEqualTo("/api/v4/users/u1"))
.willReturn(okJson("{\"username\":\"user1\"}")));
List<PostView> posts = client.getRecentPosts("chan001mj", 60);
assertThat(posts.get(0).files()).hasSize(1);
FileRef file = posts.get(0).files().get(0);
assertThat(file.id()).isEqualTo("f1");
assertThat(file.name()).isEqualTo("보고서.pdf");
assertThat(file.size()).isEqualTo(1234L);
}
@Test
void 같은_작성자의_게시글_두개면_사용자조회는_한번만_한다() {
server.stubFor(get(urlPathEqualTo("/api/v4/channels/chan001mj/posts"))
.withQueryParam("per_page", equalTo("60"))
.willReturn(okJson("""
{
"order": ["p2", "p1"],
"posts": {
"p1": {"id":"p1","user_id":"u1","message":"첫번째","create_at":100,"type":""},
"p2": {"id":"p2","user_id":"u1","message":"두번째","create_at":200,"type":""}
}
}
""")));
server.stubFor(get(urlPathEqualTo("/api/v4/users/u1"))
.willReturn(okJson("{\"username\":\"user1\",\"nickname\":\"유저원\"}")));
List<PostView> posts = client.getRecentPosts("chan001mj", 60);
assertThat(posts).extracting(PostView::user).containsExactly("유저원", "유저원");
server.verify(1, getRequestedFor(urlPathEqualTo("/api/v4/users/u1")));
}
@Test
void 파일목록_조회는_두번째_페이지까지_모아_최신순으로_돌려준다() {
server.stubFor(get(urlPathEqualTo("/api/v4/channels/chan001mj/posts"))
.withQueryParam("per_page", equalTo("200"))
.withQueryParam("page", equalTo("0"))
.willReturn(okJson(postsPageJson(200, "p0", true))));
server.stubFor(get(urlPathEqualTo("/api/v4/channels/chan001mj/posts"))
.withQueryParam("per_page", equalTo("200"))
.withQueryParam("page", equalTo("1"))
.willReturn(okJson(postsPageJson(5, "p1", true))));
server.stubFor(get(urlPathEqualTo("/api/v4/users/uploader"))
.willReturn(okJson("{\"username\":\"uploader\"}")));
List<FileView> files = client.collectChannelFiles("chan001mj");
assertThat(files).extracting(FileView::name).containsExactly("p0-file.txt", "p1-file.txt");
}
/**
* 채널 게시글 목록 한 페이지를 흉내낸 JSON을 만든다. 마지막 게시글에 첨부파일을 하나 붙여
* 페이지네이션과 파일 수집을 동시에 검증할 수 있게 한다.
*/
private static String postsPageJson(int count, String idPrefix, boolean withFile) {
StringBuilder order = new StringBuilder("[");
StringBuilder posts = new StringBuilder("{");
for (int i = 0; i < count; i++) {
String id = idPrefix + "-" + i;
if (i > 0) {
order.append(",");
posts.append(",");
}
order.append("\"").append(id).append("\"");
posts.append("\"").append(id).append("\":{\"id\":\"").append(id)
.append("\",\"user_id\":\"uploader\",\"message\":\"\",\"create_at\":1,\"type\":\"\"");
if (withFile && i == count - 1) {
posts.append(",\"metadata\":{\"files\":[{\"id\":\"").append(id)
.append("-file\",\"name\":\"").append(idPrefix)
.append("-file.txt\",\"size\":10,\"mime_type\":\"text/plain\"}]}");
}
posts.append("}");
}
order.append("]");
posts.append("}");
return "{\"order\":" + order + ",\"posts\":" + posts + "}";
}
@Test
void 파일을_다운로드하면_바이트를_그대로_돌려준다() {
server.stubFor(get(urlEqualTo("/api/v4/files/f1"))
.willReturn(aResponse().withStatus(200).withBody("binary-content")));
byte[] bytes = client.downloadFile("f1");
assertThat(new String(bytes)).isEqualTo("binary-content");
}
@Test
void 파일_정보를_조회하면_이름과_크기와_MIME타입을_돌려준다() {
server.stubFor(get(urlEqualTo("/api/v4/files/f1/info"))
.willReturn(okJson("{\"name\":\"보고서.pdf\",\"size\":1234,\"mime_type\":\"application/pdf\"}")));
FileRef info = client.fileInfo("f1");
assertThat(info.id()).isEqualTo("f1");
assertThat(info.name()).isEqualTo("보고서.pdf");
assertThat(info.size()).isEqualTo(1234L);
assertThat(info.mimeType()).isEqualTo("application/pdf");
}
}