diff --git a/connectors/slack/element-templates/hybrid/slack-outbound-connector-hybrid.json b/connectors/slack/element-templates/hybrid/slack-outbound-connector-hybrid.json
index 187dcfddec..fbb82675dc 100644
--- a/connectors/slack/element-templates/hybrid/slack-outbound-connector-hybrid.json
+++ b/connectors/slack/element-templates/hybrid/slack-outbound-connector-hybrid.json
@@ -7,7 +7,7 @@
"keywords" : [ ]
},
"documentationRef" : "https://docs.camunda.io/docs/components/connectors/out-of-the-box-connectors/slack/?slack=outbound",
- "version" : 5,
+ "version" : 6,
"category" : {
"id" : "connectors",
"name" : "Connectors"
@@ -168,6 +168,23 @@
} ]
},
"type" : "String"
+ }, {
+ "id" : "data.documents",
+ "label" : "attachments",
+ "description" : "Camunda documents can be added as attachments",
+ "optional" : true,
+ "feel" : "required",
+ "group" : "message",
+ "binding" : {
+ "name" : "data.documents",
+ "type" : "zeebe:input"
+ },
+ "condition" : {
+ "property" : "method",
+ "equals" : "chat.postMessage",
+ "type" : "simple"
+ },
+ "type" : "String"
}, {
"id" : "data.channel",
"label" : "Channel/user name/email",
diff --git a/connectors/slack/element-templates/slack-outbound-connector.json b/connectors/slack/element-templates/slack-outbound-connector.json
index 98d08e412b..386a3bdf16 100644
--- a/connectors/slack/element-templates/slack-outbound-connector.json
+++ b/connectors/slack/element-templates/slack-outbound-connector.json
@@ -7,7 +7,7 @@
"keywords" : [ ]
},
"documentationRef" : "https://docs.camunda.io/docs/components/connectors/out-of-the-box-connectors/slack/?slack=outbound",
- "version" : 5,
+ "version" : 6,
"category" : {
"id" : "connectors",
"name" : "Connectors"
@@ -163,6 +163,23 @@
} ]
},
"type" : "String"
+ }, {
+ "id" : "data.documents",
+ "label" : "attachments",
+ "description" : "Camunda documents can be added as attachments",
+ "optional" : true,
+ "feel" : "required",
+ "group" : "message",
+ "binding" : {
+ "name" : "data.documents",
+ "type" : "zeebe:input"
+ },
+ "condition" : {
+ "property" : "method",
+ "equals" : "chat.postMessage",
+ "type" : "simple"
+ },
+ "type" : "String"
}, {
"id" : "data.channel",
"label" : "Channel/user name/email",
diff --git a/connectors/slack/src/main/java/io/camunda/connector/slack/outbound/SlackFunction.java b/connectors/slack/src/main/java/io/camunda/connector/slack/outbound/SlackFunction.java
index 88fb9e36fa..50c377ab3d 100644
--- a/connectors/slack/src/main/java/io/camunda/connector/slack/outbound/SlackFunction.java
+++ b/connectors/slack/src/main/java/io/camunda/connector/slack/outbound/SlackFunction.java
@@ -17,7 +17,7 @@
name = "Slack Outbound Connector",
description = "Create a channel or send a message to a channel or user",
inputDataClass = SlackRequest.class,
- version = 5,
+ version = 6,
propertyGroups = {
@ElementTemplate.PropertyGroup(id = "authentication", label = "Authentication"),
@ElementTemplate.PropertyGroup(id = "method", label = "Method"),
diff --git a/connectors/slack/src/main/java/io/camunda/connector/slack/outbound/caller/FileUploader.java b/connectors/slack/src/main/java/io/camunda/connector/slack/outbound/caller/FileUploader.java
new file mode 100644
index 0000000000..79da93dbf4
--- /dev/null
+++ b/connectors/slack/src/main/java/io/camunda/connector/slack/outbound/caller/FileUploader.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
+ * under one or more contributor license agreements. Licensed under a proprietary license.
+ * See the License.txt file for more information. You may not use this file
+ * except in compliance with the proprietary license.
+ */
+package io.camunda.connector.slack.outbound.caller;
+
+import static java.util.stream.Collectors.toList;
+
+import com.slack.api.methods.MethodsClient;
+import com.slack.api.methods.SlackApiException;
+import com.slack.api.methods.request.files.FilesCompleteUploadExternalRequest;
+import com.slack.api.methods.request.files.FilesGetUploadURLExternalRequest;
+import com.slack.api.methods.response.files.FilesCompleteUploadExternalResponse;
+import com.slack.api.methods.response.files.FilesGetUploadURLExternalResponse;
+import com.slack.api.model.File;
+import com.slack.api.util.http.SlackHttpClient;
+import io.camunda.document.Document;
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class FileUploader {
+
+ public static final String GET_EXTERNAL_URL_EX = "Error during filesGetUploadURLExternal call";
+ public static final String EXTERNAL_URL_CALL_EX = "Error during external call: ";
+ public static final String COMPLETE_UPLOAD_CALL_EX =
+ "Error during filesCompleteUploadExternal call";
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(FileUploader.class);
+
+ private MethodsClient methodsClient;
+
+ public FileUploader(MethodsClient methodsClient) {
+ this.methodsClient = methodsClient;
+ }
+
+ public List uploadDocuments(List documents) {
+ return documents.stream()
+ .map(
+ doc -> {
+ try {
+ return this.uploadDocument(methodsClient, doc);
+ } catch (SlackApiException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ })
+ .filter(Objects::nonNull)
+ .collect(toList());
+ }
+
+ private File uploadDocument(MethodsClient methodsClient, Document document)
+ throws SlackApiException, IOException {
+ FilesGetUploadURLExternalResponse externalURLUploadResponse =
+ getFileUploadURL(methodsClient, document);
+ uploadFileByURL(externalURLUploadResponse, document);
+ return completeFileUpload(externalURLUploadResponse, document);
+ }
+
+ private FilesGetUploadURLExternalResponse getFileUploadURL(
+ MethodsClient methodsClient, Document document) throws SlackApiException, IOException {
+ var filesGetUploadURLExternalRequest =
+ FilesGetUploadURLExternalRequest.builder()
+ .filename(document.metadata().getFileName())
+ .length(document.asByteArray().length)
+ .build();
+ return methodsClient.filesGetUploadURLExternal(filesGetUploadURLExternalRequest);
+ }
+
+ private void uploadFileByURL(
+ FilesGetUploadURLExternalResponse uploadFileURLResp, Document document) throws IOException {
+ if (!uploadFileURLResp.isOk()) {
+ String msg = GET_EXTERNAL_URL_EX + "\n Errors: " + uploadFileURLResp.getError();
+ LOGGER.error(msg);
+ throw new RuntimeException(msg);
+ }
+ var config = methodsClient.getSlackHttpClient().getConfig();
+ OkHttpClient okHttpClient = SlackHttpClient.buildOkHttpClient(config);
+
+ var request =
+ new Request.Builder()
+ .url(uploadFileURLResp.getUploadUrl())
+ .post(RequestBody.create(document.asByteArray()))
+ .build();
+
+ try (Response directCallResp = okHttpClient.newCall(request).execute()) {
+ if (directCallResp.code() != 200) {
+ String msg = EXTERNAL_URL_CALL_EX + directCallResp.message();
+ LOGGER.error(msg);
+ throw new RuntimeException(msg);
+ }
+ }
+ }
+
+ private File completeFileUpload(
+ FilesGetUploadURLExternalResponse uploadFileUrlResp, Document document)
+ throws SlackApiException, IOException {
+ FilesCompleteUploadExternalResponse completeUploadResp =
+ methodsClient.filesCompleteUploadExternal(
+ FilesCompleteUploadExternalRequest.builder()
+ .files(
+ List.of(
+ FilesCompleteUploadExternalRequest.FileDetails.builder()
+ .id(uploadFileUrlResp.getFileId())
+ .title(document.metadata().getFileName())
+ .build()))
+ .build());
+
+ if (completeUploadResp.isOk()) {
+ List files = completeUploadResp.getFiles();
+ return getFirst(files);
+ } else {
+ String msg = COMPLETE_UPLOAD_CALL_EX + "\n Errors: " + completeUploadResp.getError();
+ LOGGER.error(msg);
+ throw new RuntimeException(msg);
+ }
+ }
+
+ // In fact, we always have only one file in List
+ private File getFirst(List files) {
+ return files == null || files.isEmpty() ? null : files.getFirst();
+ }
+
+ public void setMethodsClient(MethodsClient methodsClient) {
+ this.methodsClient = methodsClient;
+ }
+
+ public MethodsClient getMethodsClient() {
+ return methodsClient;
+ }
+}
diff --git a/connectors/slack/src/main/java/io/camunda/connector/slack/outbound/mapper/BlocksMapper.java b/connectors/slack/src/main/java/io/camunda/connector/slack/outbound/mapper/BlocksMapper.java
new file mode 100644
index 0000000000..d912ca3a00
--- /dev/null
+++ b/connectors/slack/src/main/java/io/camunda/connector/slack/outbound/mapper/BlocksMapper.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
+ * under one or more contributor license agreements. Licensed under a proprietary license.
+ * See the License.txt file for more information. You may not use this file
+ * except in compliance with the proprietary license.
+ */
+package io.camunda.connector.slack.outbound.mapper;
+
+import static java.util.stream.Collectors.toList;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.slack.api.model.File;
+import com.slack.api.model.block.ContextBlock;
+import com.slack.api.model.block.FileBlock;
+import com.slack.api.model.block.LayoutBlock;
+import com.slack.api.model.block.composition.PlainTextObject;
+import com.slack.api.util.json.GsonFactory;
+import io.camunda.connector.api.error.ConnectorException;
+import java.util.ArrayList;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class BlocksMapper {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(BlocksMapper.class);
+ public static final String REMOTE_FILE_SOURCE = "remote";
+
+ private BlocksMapper() {}
+
+ public static List mapBlocks(List files, String text, JsonNode blockContent) {
+ List blocks = mapBlocks(text, blockContent);
+ blocks.addAll(prepareFileBlocks(files));
+ return blocks;
+ }
+
+ public static List mapBlocks(String text, JsonNode blockContent) {
+ List blocks = new ArrayList<>();
+ if (blockContent != null) {
+ blocks = parseBlockContent(blockContent);
+ } else {
+ blocks.add(prepareBlockFromMessage(text));
+ }
+
+ return blocks;
+ }
+
+ private static LayoutBlock prepareBlockFromMessage(String text) {
+ return ContextBlock.builder()
+ .elements(List.of(PlainTextObject.builder().text(text).build()))
+ .build();
+ }
+
+ private static List prepareFileBlocks(List files) {
+ return files.stream()
+ .map(file -> FileBlock.builder().fileId(file.getId()).source(REMOTE_FILE_SOURCE).build())
+ .collect(toList());
+ }
+
+ private static List parseBlockContent(JsonNode blockContent) {
+ if (!blockContent.isArray()) {
+ String msg = "Block section must be an array";
+ LOGGER.warn(msg);
+ throw new ConnectorException(msg);
+ }
+
+ ArrayNode arrayNode = (ArrayNode) blockContent;
+ List blocks = new ArrayList<>();
+ for (JsonNode node : arrayNode) {
+ blocks.add(GsonFactory.createSnakeCase().fromJson(node.toString(), LayoutBlock.class));
+ }
+ return blocks;
+ }
+}
diff --git a/connectors/slack/src/main/java/io/camunda/connector/slack/outbound/model/ChatPostMessageData.java b/connectors/slack/src/main/java/io/camunda/connector/slack/outbound/model/ChatPostMessageData.java
index 181ffa4b38..b9e6a40183 100644
--- a/connectors/slack/src/main/java/io/camunda/connector/slack/outbound/model/ChatPostMessageData.java
+++ b/connectors/slack/src/main/java/io/camunda/connector/slack/outbound/model/ChatPostMessageData.java
@@ -6,12 +6,16 @@
*/
package io.camunda.connector.slack.outbound.model;
+import static io.camunda.connector.slack.outbound.mapper.BlocksMapper.mapBlocks;
+
import com.fasterxml.jackson.databind.JsonNode;
import com.slack.api.methods.MethodsClient;
import com.slack.api.methods.SlackApiException;
import com.slack.api.methods.request.chat.ChatPostMessageRequest;
import com.slack.api.methods.response.chat.ChatPostMessageResponse;
+import com.slack.api.model.File;
import io.camunda.connector.api.error.ConnectorException;
+import io.camunda.connector.generator.dsl.Property;
import io.camunda.connector.generator.dsl.Property.FeelMode;
import io.camunda.connector.generator.java.annotation.TemplateProperty;
import io.camunda.connector.generator.java.annotation.TemplateProperty.PropertyBinding;
@@ -19,10 +23,13 @@
import io.camunda.connector.generator.java.annotation.TemplateProperty.PropertyType;
import io.camunda.connector.generator.java.annotation.TemplateSubType;
import io.camunda.connector.slack.outbound.SlackResponse;
+import io.camunda.connector.slack.outbound.caller.FileUploader;
import io.camunda.connector.slack.outbound.utils.DataLookupService;
+import io.camunda.document.Document;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
import java.io.IOException;
+import java.util.List;
import org.apache.commons.lang3.StringUtils;
@TemplateSubType(id = "chat.postMessage", label = "Post message")
@@ -91,10 +98,21 @@ public record ChatPostMessageData(
equals = "messageBlock"),
defaultValue =
"=[\n\t{\n\t\t\"type\": \"header\",\n\t\t\"text\": {\n\t\t\t\"type\": \"plain_text\",\n\t\t\t\"text\": \"New request\"\n\t\t}\n\t},\n\t{\n\t\t\"type\": \"section\",\n\t\t\"fields\": [\n\t\t\t{\n\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\"text\": \"*Type:*\\nPaid Time Off\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\"text\": \"*Created by:*\\n\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"type\": \"section\",\n\t\t\"fields\": [\n\t\t\t{\n\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\"text\": \"*When:*\\nAug 10 - Aug 13\"\n\t\t\t}\n\t\t]\n\t},\n\t{\n\t\t\"type\": \"section\",\n\t\t\"text\": {\n\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\"text\": \"\"\n\t\t}\n\t}\n]")
- JsonNode blockContent)
+ JsonNode blockContent,
+ @TemplateProperty(
+ id = "data.documents",
+ group = "message",
+ label = "attachments",
+ feel = Property.FeelMode.required,
+ binding = @PropertyBinding(name = "data.documents"),
+ type = TemplateProperty.PropertyType.String,
+ optional = true,
+ description =
+ "Camunda documents can be added as attachments")
+ List documents)
implements SlackRequestData {
@Override
- public SlackResponse invoke(MethodsClient methodsClient) throws SlackApiException, IOException {
+ public SlackResponse invoke(MethodsClient methodsClient) throws IOException, SlackApiException {
if (!isContentSupplied()) {
throw new ConnectorException("Text or block content required to post a message");
}
@@ -108,20 +126,16 @@ public SlackResponse invoke(MethodsClient methodsClient) throws SlackApiExceptio
var requestBuilder = ChatPostMessageRequest.builder().channel(filteredChannel);
- // Note: both text and block content can co-exist
- if (StringUtils.isNotBlank(text)) {
- requestBuilder.text(text);
- // Enables plain text message formatting
- requestBuilder.linkNames(true);
- }
if (StringUtils.isNotBlank(thread)) {
requestBuilder.threadTs(thread);
}
- if (blockContent != null) {
- if (!blockContent.isArray()) {
- throw new ConnectorException("Block section must be an array");
- }
- requestBuilder.blocksAsString(blockContent.toString());
+
+ if (this.documents != null && !this.documents.isEmpty()) {
+ var fileUploader = new FileUploader(methodsClient);
+ List files = fileUploader.uploadDocuments(documents);
+ requestBuilder.blocks(mapBlocks(files, text, blockContent));
+ } else {
+ requestBuilder.blocks(mapBlocks(text, blockContent));
}
var request = requestBuilder.build();
diff --git a/connectors/slack/src/test/java/io/camunda/connector/slack/outbound/caller/FileUploaderTest.java b/connectors/slack/src/test/java/io/camunda/connector/slack/outbound/caller/FileUploaderTest.java
new file mode 100644
index 0000000000..53c5ede33a
--- /dev/null
+++ b/connectors/slack/src/test/java/io/camunda/connector/slack/outbound/caller/FileUploaderTest.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
+ * under one or more contributor license agreements. Licensed under a proprietary license.
+ * See the License.txt file for more information. You may not use this file
+ * except in compliance with the proprietary license.
+ */
+package io.camunda.connector.slack.outbound.caller;
+
+import static io.camunda.connector.slack.outbound.caller.FileUploader.*;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.slack.api.SlackConfig;
+import com.slack.api.methods.MethodsClient;
+import com.slack.api.methods.SlackApiException;
+import com.slack.api.methods.request.files.FilesCompleteUploadExternalRequest;
+import com.slack.api.methods.request.files.FilesGetUploadURLExternalRequest;
+import com.slack.api.methods.response.files.FilesCompleteUploadExternalResponse;
+import com.slack.api.methods.response.files.FilesGetUploadURLExternalResponse;
+import com.slack.api.model.File;
+import com.slack.api.util.http.SlackHttpClient;
+import io.camunda.connector.document.annotation.jackson.DocumentReferenceModel;
+import io.camunda.document.CamundaDocument;
+import io.camunda.document.Document;
+import io.camunda.document.reference.DocumentReference;
+import io.camunda.document.store.CamundaDocumentStore;
+import io.camunda.zeebe.client.api.response.DocumentMetadata;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.time.OffsetDateTime;
+import java.util.List;
+import java.util.Map;
+import okhttp3.*;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.*;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+class FileUploaderTest {
+
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private MethodsClient methodsClient;
+
+ @InjectMocks private FileUploader fileUploader;
+
+ @Test
+ void uploadDocuments() throws SlackApiException, IOException {
+ List documents = List.of(prepareDocument());
+
+ mockGetExternalURL();
+ when(methodsClient.getSlackHttpClient().getConfig()).thenReturn(SlackConfig.DEFAULT);
+
+ OkHttpClient okHttpClient = Mockito.mock(OkHttpClient.class, Answers.RETURNS_DEEP_STUBS);
+
+ try (MockedStatic slackHttpClient =
+ Mockito.mockStatic(SlackHttpClient.class)) {
+ slackHttpClient.when(() -> SlackHttpClient.buildOkHttpClient(any())).thenReturn(okHttpClient);
+
+ var response = mock(Response.class);
+ when(response.code()).thenReturn(200);
+
+ when(okHttpClient.newCall(any(Request.class)).execute()).thenReturn(response);
+
+ List files = List.of(File.builder().id("id").build());
+ mockCompleteUpload(files);
+
+ List result = fileUploader.uploadDocuments(documents);
+ assertThat(result).hasSize(1);
+ assertThat(result).isEqualTo(files);
+ }
+ }
+
+ @Test
+ void uploadDocumentsShouldFilterEmptyFiles() throws SlackApiException, IOException {
+ List documents = List.of(prepareDocument());
+ mockGetExternalURL();
+ when(methodsClient.getSlackHttpClient().getConfig()).thenReturn(SlackConfig.DEFAULT);
+
+ OkHttpClient okHttpClient = Mockito.mock(OkHttpClient.class, Answers.RETURNS_DEEP_STUBS);
+
+ try (MockedStatic slackHttpClient =
+ Mockito.mockStatic(SlackHttpClient.class)) {
+ slackHttpClient.when(() -> SlackHttpClient.buildOkHttpClient(any())).thenReturn(okHttpClient);
+
+ var response = mock(Response.class);
+ when(response.code()).thenReturn(200);
+
+ when(okHttpClient.newCall(any(Request.class)).execute()).thenReturn(response);
+
+ List emptyFiles = List.of();
+ mockCompleteUpload(emptyFiles);
+
+ List result = fileUploader.uploadDocuments(documents);
+ assertThat(result).hasSize(0);
+ }
+ }
+
+ @Test
+ void exceptionDuringGetExternalURLCall() throws SlackApiException, IOException {
+ List documents = List.of(prepareDocument());
+ var uploadURLResp = new FilesGetUploadURLExternalResponse();
+ uploadURLResp.setOk(false);
+ uploadURLResp.setError("Do not have Read permissions");
+
+ when(methodsClient.filesGetUploadURLExternal(any(FilesGetUploadURLExternalRequest.class)))
+ .thenReturn(uploadURLResp);
+
+ Exception ex =
+ assertThrows(RuntimeException.class, () -> fileUploader.uploadDocuments(documents));
+
+ assertThat(ex.getMessage()).contains(GET_EXTERNAL_URL_EX);
+ assertThat(ex.getMessage()).contains(uploadURLResp.getError());
+ }
+
+ @Test
+ void exceptionDuringExternalURlCall() throws SlackApiException, IOException {
+ List documents = List.of(prepareDocument());
+
+ var uploadURLResp = new FilesGetUploadURLExternalResponse();
+ uploadURLResp.setOk(true);
+ uploadURLResp.setUploadUrl("https:example.com");
+
+ when(methodsClient.filesGetUploadURLExternal(any(FilesGetUploadURLExternalRequest.class)))
+ .thenReturn(uploadURLResp);
+ when(methodsClient.getSlackHttpClient().getConfig()).thenReturn(SlackConfig.DEFAULT);
+ OkHttpClient okHttpClient = Mockito.mock(OkHttpClient.class, Answers.RETURNS_DEEP_STUBS);
+
+ try (MockedStatic slackHttpClient =
+ Mockito.mockStatic(SlackHttpClient.class)) {
+ slackHttpClient.when(() -> SlackHttpClient.buildOkHttpClient(any())).thenReturn(okHttpClient);
+
+ String msg = "Internal Server Error";
+
+ Response response = mock(Response.class);
+ when(response.code()).thenReturn(500);
+ when(response.message()).thenReturn(msg);
+
+ when(okHttpClient.newCall(any(Request.class)).execute()).thenReturn(response);
+
+ Exception ex =
+ assertThrows(RuntimeException.class, () -> fileUploader.uploadDocuments(documents));
+
+ assertThat(ex.getMessage()).contains(EXTERNAL_URL_CALL_EX);
+ assertThat(ex.getMessage()).contains(msg);
+ }
+ }
+
+ @Test
+ void ExceptionDuringCompleteFileUploadCall() throws SlackApiException, IOException {
+ List documents = List.of(prepareDocument());
+ var uploadURLResp = new FilesGetUploadURLExternalResponse();
+ uploadURLResp.setOk(true);
+ uploadURLResp.setUploadUrl("https:example.com");
+
+ when(methodsClient.filesGetUploadURLExternal(any(FilesGetUploadURLExternalRequest.class)))
+ .thenReturn(uploadURLResp);
+ when(methodsClient.getSlackHttpClient().getConfig()).thenReturn(SlackConfig.DEFAULT);
+
+ OkHttpClient okHttpClient = Mockito.mock(OkHttpClient.class, Answers.RETURNS_DEEP_STUBS);
+
+ try (MockedStatic slackHttpClient =
+ Mockito.mockStatic(SlackHttpClient.class)) {
+ slackHttpClient.when(() -> SlackHttpClient.buildOkHttpClient(any())).thenReturn(okHttpClient);
+
+ var response = mock(Response.class);
+ when(response.code()).thenReturn(200);
+
+ when(okHttpClient.newCall(any(Request.class)).execute()).thenReturn(response);
+
+ FilesCompleteUploadExternalResponse completeUploadResp =
+ new FilesCompleteUploadExternalResponse();
+ completeUploadResp.setOk(false);
+ completeUploadResp.setError("somthing go wrong");
+
+ when(methodsClient.filesCompleteUploadExternal(any(FilesCompleteUploadExternalRequest.class)))
+ .thenReturn(completeUploadResp);
+
+ Exception ex =
+ assertThrows(RuntimeException.class, () -> fileUploader.uploadDocuments(documents));
+
+ assertThat(ex.getMessage()).contains(completeUploadResp.getError());
+ assertThat(ex.getMessage()).contains(COMPLETE_UPLOAD_CALL_EX);
+ }
+ }
+
+ private Document prepareDocument() {
+ DocumentReference.CamundaDocumentReference documentReference =
+ Mockito.mock(DocumentReference.CamundaDocumentReference.class);
+ CamundaDocumentStore documentStore = Mockito.mock(CamundaDocumentStore.class);
+
+ var byteInput = new ByteArrayInputStream(new byte[0]);
+ when(documentStore.getDocumentContent(any())).thenReturn(byteInput);
+
+ DocumentMetadata documentMetadata =
+ new DocumentReferenceModel.CamundaDocumentMetadataModel(
+ "txt", OffsetDateTime.now(), 3000L, "fileName", "processId", 2000L, Map.of());
+
+ return new CamundaDocument(documentMetadata, documentReference, documentStore);
+ }
+
+ private void mockGetExternalURL() throws SlackApiException, IOException {
+ var uploadURLResp = new FilesGetUploadURLExternalResponse();
+ uploadURLResp.setOk(true);
+ uploadURLResp.setUploadUrl("https:example.com");
+
+ when(methodsClient.filesGetUploadURLExternal(any(FilesGetUploadURLExternalRequest.class)))
+ .thenReturn(uploadURLResp);
+ }
+
+ private void mockCompleteUpload(List filesResponse) throws SlackApiException, IOException {
+ FilesCompleteUploadExternalResponse completeUploadResp =
+ new FilesCompleteUploadExternalResponse();
+ completeUploadResp.setOk(true);
+ completeUploadResp.setFiles(filesResponse);
+
+ when(methodsClient.filesCompleteUploadExternal(any(FilesCompleteUploadExternalRequest.class)))
+ .thenReturn(completeUploadResp);
+ }
+}
diff --git a/connectors/slack/src/test/java/io/camunda/connector/slack/outbound/mapper/BlocksMapperTest.java b/connectors/slack/src/test/java/io/camunda/connector/slack/outbound/mapper/BlocksMapperTest.java
new file mode 100644
index 0000000000..ef67840c0f
--- /dev/null
+++ b/connectors/slack/src/test/java/io/camunda/connector/slack/outbound/mapper/BlocksMapperTest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
+ * under one or more contributor license agreements. Licensed under a proprietary license.
+ * See the License.txt file for more information. You may not use this file
+ * except in compliance with the proprietary license.
+ */
+package io.camunda.connector.slack.outbound.mapper;
+
+import static io.camunda.connector.slack.outbound.mapper.BlocksMapper.REMOTE_FILE_SOURCE;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.slack.api.model.File;
+import com.slack.api.model.block.*;
+import com.slack.api.model.block.composition.MarkdownTextObject;
+import com.slack.api.model.block.composition.PlainTextObject;
+import io.camunda.connector.api.json.ConnectorsObjectMapperSupplier;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class BlocksMapperTest {
+
+ private static final ObjectMapper objectMapper = ConnectorsObjectMapperSupplier.getCopy();
+
+ @Test
+ void mapBlocksWithText() {
+ String text = " just a text";
+
+ List result = BlocksMapper.mapBlocks(text, null);
+
+ List expected =
+ List.of(
+ ContextBlock.builder()
+ .elements(List.of(PlainTextObject.builder().text(text).build()))
+ .build());
+
+ assertThat(result).isEqualTo(expected);
+ }
+
+ @Test
+ void mapBlocksWithJsonNode() throws JsonProcessingException {
+
+ JsonNode jsonNode = getBlocksAsJson();
+
+ List result = BlocksMapper.mapBlocks(null, jsonNode);
+
+ List expected = getExpectedBlocks();
+
+ assertThat(result).isEqualTo(expected);
+ }
+
+ @Test
+ void mapBlocksWithFilesAndJsonNode() throws JsonProcessingException {
+ String fileId = "id";
+ List files = List.of(File.builder().id(fileId).build());
+
+ List expected = new ArrayList<>(getExpectedBlocks());
+ expected.add(FileBlock.builder().fileId(fileId).source(REMOTE_FILE_SOURCE).build());
+
+ List result = BlocksMapper.mapBlocks(files, null, getBlocksAsJson());
+
+ assertThat(result).isEqualTo(expected);
+ }
+
+ @Test
+ void mapBlocksWithFilesAndText() throws JsonProcessingException {
+ String fileId = "id";
+ List files = List.of(File.builder().id(fileId).build());
+
+ String text = "just a text";
+
+ List expected =
+ List.of(
+ ContextBlock.builder()
+ .elements(List.of(PlainTextObject.builder().text(text).build()))
+ .build(),
+ FileBlock.builder().fileId(fileId).source(REMOTE_FILE_SOURCE).build());
+
+ List result = BlocksMapper.mapBlocks(files, text, null);
+
+ assertThat(result).isEqualTo(expected);
+ }
+
+ private JsonNode getBlocksAsJson() throws JsonProcessingException {
+ String blocks =
+ """
+ [
+ {
+ "type": "header",
+ "text": {
+ "type": "plain_text",
+ "text": "New request"
+ }
+ },
+ {
+ "type": "section",
+ "fields": [
+ {
+ "type": "mrkdwn",
+ "text": "*When:* Aug 10 - Aug 13"
+ }
+ ]
+ }
+ ]
+ """;
+
+ return objectMapper.readTree(blocks);
+ }
+
+ private List getExpectedBlocks() {
+ return List.of(
+ HeaderBlock.builder().text(PlainTextObject.builder().text("New request").build()).build(),
+ SectionBlock.builder()
+ .fields(List.of(MarkdownTextObject.builder().text("*When:* Aug 10 - Aug 13").build()))
+ .build());
+ }
+}
diff --git a/connectors/slack/src/test/java/io/camunda/connector/slack/outbound/model/ChatPostMessageDataTest.java b/connectors/slack/src/test/java/io/camunda/connector/slack/outbound/model/ChatPostMessageDataTest.java
index 26c2739288..b28e8bcfc2 100644
--- a/connectors/slack/src/test/java/io/camunda/connector/slack/outbound/model/ChatPostMessageDataTest.java
+++ b/connectors/slack/src/test/java/io/camunda/connector/slack/outbound/model/ChatPostMessageDataTest.java
@@ -9,6 +9,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.catchThrowable;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -16,27 +17,46 @@
import com.slack.api.methods.MethodsClient;
import com.slack.api.methods.SlackApiException;
import com.slack.api.methods.request.chat.ChatPostMessageRequest;
+import com.slack.api.methods.request.files.FilesCompleteUploadExternalRequest;
+import com.slack.api.methods.request.files.FilesGetUploadURLExternalRequest;
import com.slack.api.methods.request.users.UsersLookupByEmailRequest;
import com.slack.api.methods.response.chat.ChatPostMessageResponse;
+import com.slack.api.methods.response.files.FilesCompleteUploadExternalResponse;
+import com.slack.api.methods.response.files.FilesGetUploadURLExternalResponse;
import com.slack.api.methods.response.users.UsersLookupByEmailResponse;
+import com.slack.api.model.File;
import com.slack.api.model.Message;
import com.slack.api.model.User;
+import com.slack.api.util.http.SlackHttpClient;
import io.camunda.connector.api.error.ConnectorException;
import io.camunda.connector.api.json.ConnectorsObjectMapperSupplier;
+import io.camunda.connector.document.annotation.jackson.DocumentReferenceModel;
+import io.camunda.document.CamundaDocument;
+import io.camunda.document.Document;
+import io.camunda.document.reference.DocumentReference;
+import io.camunda.document.store.CamundaDocumentStore;
+import io.camunda.zeebe.client.api.response.DocumentMetadata;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.time.OffsetDateTime;
+import java.util.List;
+import java.util.Map;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
+import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class ChatPostMessageDataTest {
- @Mock private MethodsClient methodsClient;
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private MethodsClient methodsClient;
+
@Mock private UsersLookupByEmailResponse lookupByEmailResponse;
@Mock private User user;
@Mock private ChatPostMessageResponse chatPostMessageResponse;
@@ -58,7 +78,8 @@ class ChatPostMessageDataTest {
void invoke_shouldThrowExceptionWhenUserWithoutEmail() throws SlackApiException, IOException {
// Given
ChatPostMessageData chatPostMessageData =
- new ChatPostMessageData("test@test.com", "thread_ts", "plainText", "Test text", EMPTY_JSON);
+ new ChatPostMessageData(
+ "test@test.com", "thread_ts", "plainText", "Test text", EMPTY_JSON, List.of());
when(methodsClient.usersLookupByEmail(any(UsersLookupByEmailRequest.class))).thenReturn(null);
// When and then
Throwable thrown = catchThrowable(() -> chatPostMessageData.invoke(methodsClient));
@@ -80,7 +101,7 @@ void invoke_shouldThrowExceptionWhenUserWithoutEmail() throws SlackApiException,
void invoke_shouldFindUserIdByEmail(String email) throws SlackApiException, IOException {
// Given
ChatPostMessageData chatPostMessageData =
- new ChatPostMessageData(email, "thread_ts", "plainText", "test", null);
+ new ChatPostMessageData(email, "thread_ts", "plainText", "test", null, List.of());
when(methodsClient.usersLookupByEmail(any(UsersLookupByEmailRequest.class)))
.thenReturn(lookupByEmailResponse);
@@ -102,22 +123,42 @@ void invoke_shouldFindUserIdByEmail(String email) throws SlackApiException, IOEx
void invoke_WhenTextIsGiven_ShouldInvoke() throws SlackApiException, IOException {
// Given
ChatPostMessageData chatPostMessageData =
- new ChatPostMessageData("test@test.com", "thread_ts", "plainText", "test", null);
+ new ChatPostMessageData(
+ "test@test.com", "thread_ts", "plainText", "test", null, List.of(prepareDocument()));
when(methodsClient.usersLookupByEmail(any(UsersLookupByEmailRequest.class)))
.thenReturn(lookupByEmailResponse);
when(lookupByEmailResponse.isOk()).thenReturn(Boolean.TRUE);
when(lookupByEmailResponse.getUser()).thenReturn(user);
when(user.getId()).thenReturn(USERID);
- when(methodsClient.chatPostMessage(chatPostMessageRequest.capture()))
- .thenReturn(chatPostMessageResponse);
- when(chatPostMessageResponse.isOk()).thenReturn(true);
- when(chatPostMessageResponse.getMessage()).thenReturn(new Message());
- // When
- chatPostMessageData.invoke(methodsClient);
- // Then
- ChatPostMessageRequest value = chatPostMessageRequest.getValue();
- assertThat(value.getChannel()).isEqualTo(USERID);
+
+ // mock File uploading
+ mockGetExternalURL();
+
+ OkHttpClient okHttpClient = Mockito.mock(OkHttpClient.class, Answers.RETURNS_DEEP_STUBS);
+
+ try (MockedStatic slackHttpClient =
+ Mockito.mockStatic(SlackHttpClient.class)) {
+ slackHttpClient.when(() -> SlackHttpClient.buildOkHttpClient(any())).thenReturn(okHttpClient);
+
+ var response = mock(Response.class);
+ when(response.code()).thenReturn(200);
+
+ when(okHttpClient.newCall(any(Request.class)).execute()).thenReturn(response);
+
+ mockCompleteExternalURL();
+
+ when(methodsClient.chatPostMessage(chatPostMessageRequest.capture()))
+ .thenReturn(chatPostMessageResponse);
+
+ when(chatPostMessageResponse.isOk()).thenReturn(true);
+ when(chatPostMessageResponse.getMessage()).thenReturn(new Message());
+ // When
+ chatPostMessageData.invoke(methodsClient);
+ // Then
+ ChatPostMessageRequest value = chatPostMessageRequest.getValue();
+ assertThat(value.getChannel()).isEqualTo(USERID);
+ }
}
@Test
@@ -173,7 +214,8 @@ void invoke_WhenContentBlockIsGiven_ShouldInvoke() throws SlackApiException, IOE
"thread_ts",
"messageBlock",
"test",
- objectMapper.readTree(blockContent));
+ objectMapper.readTree(blockContent),
+ List.of());
when(methodsClient.usersLookupByEmail(any(UsersLookupByEmailRequest.class)))
.thenReturn(lookupByEmailResponse);
@@ -209,7 +251,12 @@ void invoke_WhenContentBlockIsNotArray_ShouldThrow() throws SlackApiException, I
ChatPostMessageData chatPostMessageData =
new ChatPostMessageData(
- "test@test.com", "thread_ts", "plainText", "test", objectMapper.readTree(blockContent));
+ "test@test.com",
+ "thread_ts",
+ "plainText",
+ "test",
+ objectMapper.readTree(blockContent),
+ List.of());
when(methodsClient.usersLookupByEmail(any(UsersLookupByEmailRequest.class)))
.thenReturn(lookupByEmailResponse);
@@ -223,4 +270,39 @@ void invoke_WhenContentBlockIsNotArray_ShouldThrow() throws SlackApiException, I
assertThat(thrown).hasMessageContaining("Block section must be an array");
assertThat(thrown).isInstanceOf(ConnectorException.class);
}
+
+ private Document prepareDocument() {
+ DocumentReference.CamundaDocumentReference documentReference =
+ Mockito.mock(DocumentReference.CamundaDocumentReference.class);
+ CamundaDocumentStore documentStore = Mockito.mock(CamundaDocumentStore.class);
+
+ var byteInput = new ByteArrayInputStream(new byte[0]);
+ when(documentStore.getDocumentContent(any())).thenReturn(byteInput);
+
+ DocumentMetadata documentMetadata =
+ new DocumentReferenceModel.CamundaDocumentMetadataModel(
+ "txt", OffsetDateTime.now(), 3000L, "fileName", "processId", 2000L, Map.of());
+
+ return new CamundaDocument(documentMetadata, documentReference, documentStore);
+ }
+
+ private void mockGetExternalURL() throws SlackApiException, IOException {
+ var uploadURLResp = new FilesGetUploadURLExternalResponse();
+ uploadURLResp.setOk(true);
+ uploadURLResp.setUploadUrl("https:example.com");
+
+ when(methodsClient.filesGetUploadURLExternal(any(FilesGetUploadURLExternalRequest.class)))
+ .thenReturn(uploadURLResp);
+ }
+
+ private void mockCompleteExternalURL() throws SlackApiException, IOException {
+ List files = List.of(File.builder().id("id").build());
+ FilesCompleteUploadExternalResponse completeUploadResp =
+ new FilesCompleteUploadExternalResponse();
+ completeUploadResp.setOk(true);
+ completeUploadResp.setFiles(files);
+
+ when(methodsClient.filesCompleteUploadExternal(any(FilesCompleteUploadExternalRequest.class)))
+ .thenReturn(completeUploadResp);
+ }
}