You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by ma...@apache.org on 2019/03/07 18:01:50 UTC
[nifi] branch master updated: NIFI-6078: Create PostSlack processor
This is an automated email from the ASF dual-hosted git repository.
mattyb149 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/master by this push:
new 79e05c9 NIFI-6078: Create PostSlack processor
79e05c9 is described below
commit 79e05c9f583aa1e4d390f380e375253bd7be198e
Author: Peter Turcsanyi <tu...@cloudera.com>
AuthorDate: Thu Feb 21 00:27:43 2019 +0100
NIFI-6078: Create PostSlack processor
Processor for sending messages on Slack and optionally upload and attach
the FlowFile content (e.g. an image) to the message.
NIFI-6078: Remove username/icon properties.
NIFI-6078: Make Text property optional.
NIFI-6078: Documentation changes.
Signed-off-by: Matthew Burgess <ma...@apache.org>
This closes #3339
---
.../src/main/resources/META-INF/NOTICE | 42 ++
.../nifi-slack-processors/pom.xml | 10 +
.../apache/nifi/processors/slack/PostSlack.java | 474 +++++++++++++++++++++
.../services/org.apache.nifi.processor.Processor | 3 +-
.../additionalDetails.html | 107 +++++
.../processors/slack/PostSlackCaptureServlet.java | 103 +++++
.../slack/PostSlackConfigValidationTest.java | 152 +++++++
.../processors/slack/PostSlackFileMessageTest.java | 312 ++++++++++++++
.../processors/slack/PostSlackTextMessageTest.java | 282 ++++++++++++
9 files changed, 1484 insertions(+), 1 deletion(-)
diff --git a/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-nar/src/main/resources/META-INF/NOTICE b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-nar/src/main/resources/META-INF/NOTICE
index 53ea274..f3e9cb8 100644
--- a/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-nar/src/main/resources/META-INF/NOTICE
+++ b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-nar/src/main/resources/META-INF/NOTICE
@@ -4,6 +4,48 @@ Copyright 2016-2019 The Apache Software Foundation
This product includes software developed at
The Apache Software Foundation (http://www.apache.org/).
+******************
+Apache Software License v2
+******************
+
+The following binary components are provided under the Apache Software License v2
+
+ (ASLv2) Apache Commons Codec
+ The following NOTICE information applies:
+ Apache Commons Codec
+ Copyright 2002-2017 The Apache Software Foundation
+
+ src/test/org/apache/commons/codec/language/DoubleMetaphoneTest.java
+ contains test data from http://aspell.net/test/orig/batch0.tab.
+ Copyright (C) 2002 Kevin Atkinson (kevina@gnu.org)
+
+ ===============================================================================
+
+ The content of package org.apache.commons.codec.language.bm has been translated
+ from the original php source code available at http://stevemorse.org/phoneticinfo.htm
+ with permission from the original authors.
+ Original source copyright:
+ Copyright (c) 2008 Alexander Beider & Stephen P. Morse.
+
+ (ASLv2) Apache Commons Logging
+ The following NOTICE information applies:
+ Apache Commons Logging
+ Copyright 2003-2014 The Apache Software Foundation
+
+ (ASLv2) Apache HttpComponents
+ The following NOTICE information applies:
+ Apache HttpClient
+ Copyright 1999-2019 The Apache Software Foundation
+
+ Apache HttpCore
+ Copyright 2005-2019 The Apache Software Foundation
+
+ Apache HttpMime
+ Copyright 1999-2019 The Apache Software Foundation
+
+ This project contains annotations derived from JCIP-ANNOTATIONS
+ Copyright (c) 2005 Brian Goetz and Tim Peierls. See http://www.jcip.net
+
************************
Common Development and Distribution License 1.1
************************
diff --git a/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/pom.xml b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/pom.xml
index a6aa3b5..eb10033 100644
--- a/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/pom.xml
+++ b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/pom.xml
@@ -37,6 +37,16 @@
<version>1.0.4</version>
</dependency>
<dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ <version>4.5.7</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpmime</artifactId>
+ <version>4.5.7</version>
+ </dependency>
+ <dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-api</artifactId>
</dependency>
diff --git a/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/main/java/org/apache/nifi/processors/slack/PostSlack.java b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/main/java/org/apache/nifi/processors/slack/PostSlack.java
new file mode 100644
index 0000000..f581731
--- /dev/null
+++ b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/main/java/org/apache/nifi/processors/slack/PostSlack.java
@@ -0,0 +1,474 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.processors.slack;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHeaders;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.http.util.EntityUtils;
+import org.apache.nifi.annotation.behavior.DynamicProperty;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.WritesAttribute;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.annotation.lifecycle.OnStopped;
+import org.apache.nifi.components.AllowableValue;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.ValidationContext;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.expression.AttributeExpression;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import javax.json.Json;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonString;
+import javax.json.stream.JsonParsingException;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
+@Tags({"slack", "post", "notify", "upload", "message"})
+@CapabilityDescription("Sends a message on Slack. The FlowFile content (e.g. an image) can be uploaded and attached to the message.")
+@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
+@DynamicProperty(name = "<Arbitrary name>", value = "JSON snippet specifying a Slack message \"attachment\"",
+ description = "The property value will be converted to JSON and will be added to the array of attachments in the JSON payload being sent to Slack." +
+ " The property name will not be used by the processor.",
+ expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+@WritesAttribute(attribute="slack.file.url", description = "The Slack URL of the uploaded file. It will be added if 'Upload FlowFile' has been set to 'Yes'.")
+public class PostSlack extends AbstractProcessor {
+
+ private static final String SLACK_POST_MESSAGE_URL = "https://slack.com/api/chat.postMessage";
+
+ private static final String SLACK_FILE_UPLOAD_URL = "https://slack.com/api/files.upload";
+
+ public static final PropertyDescriptor POST_MESSAGE_URL = new PropertyDescriptor.Builder()
+ .name("post-message-url")
+ .displayName("Post Message URL")
+ .description("Slack Web API URL for posting text messages to channels." +
+ " It only needs to be changed if Slack changes its API URL.")
+ .required(true)
+ .defaultValue(SLACK_POST_MESSAGE_URL)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .addValidator(StandardValidators.URL_VALIDATOR)
+ .build();
+
+ public static final PropertyDescriptor FILE_UPLOAD_URL = new PropertyDescriptor.Builder()
+ .name("file-upload-url")
+ .displayName("File Upload URL")
+ .description("Slack Web API URL for uploading files to channels." +
+ " It only needs to be changed if Slack changes its API URL.")
+ .required(true)
+ .defaultValue(SLACK_FILE_UPLOAD_URL)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .addValidator(StandardValidators.URL_VALIDATOR)
+ .build();
+
+ public static final PropertyDescriptor ACCESS_TOKEN = new PropertyDescriptor.Builder()
+ .name("access-token")
+ .displayName("Access Token")
+ .description("OAuth Access Token used for authenticating/authorizing the Slack request sent by NiFi.")
+ .required(true)
+ .sensitive(true)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .build();
+
+ public static final PropertyDescriptor CHANNEL = new PropertyDescriptor.Builder()
+ .name("channel")
+ .displayName("Channel")
+ .description("Slack channel, private group, or IM channel to send the message to.")
+ .required(true)
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+ .build();
+
+ public static final PropertyDescriptor TEXT = new PropertyDescriptor.Builder()
+ .name("text")
+ .displayName("Text")
+ .description("Text of the Slack message to send. Only required if no attachment has been specified and 'Upload File' has been set to 'No'.")
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+ .build();
+
+ public static final AllowableValue UPLOAD_FLOWFILE_YES = new AllowableValue(
+ "true",
+ "Yes",
+ "Upload and attach FlowFile content to the Slack message."
+ );
+
+ public static final AllowableValue UPLOAD_FLOWFILE_NO = new AllowableValue(
+ "false",
+ "No",
+ "Don't upload and attach FlowFile content to the Slack message."
+ );
+
+ public static final PropertyDescriptor UPLOAD_FLOWFILE = new PropertyDescriptor.Builder()
+ .name("upload-flowfile")
+ .displayName("Upload FlowFile")
+ .description("Whether or not to upload and attach the FlowFile content to the Slack message.")
+ .allowableValues(UPLOAD_FLOWFILE_YES, UPLOAD_FLOWFILE_NO)
+ .required(true)
+ .defaultValue("false")
+ .build();
+
+ public static final PropertyDescriptor FILE_TITLE = new PropertyDescriptor.Builder()
+ .name("file-title")
+ .displayName("File Title")
+ .description("Title of the file displayed in the Slack message." +
+ " The property value will only be used if 'Upload FlowFile' has been set to 'Yes'.")
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+ .build();
+
+ public static final PropertyDescriptor FILE_NAME = new PropertyDescriptor.Builder()
+ .name("file-name")
+ .displayName("File Name")
+ .description("Name of the file to be uploaded." +
+ " The property value will only be used if 'Upload FlowFile' has been set to 'Yes'." +
+ " If the property evaluated to null or empty string, then the file name will be set to 'file' in the Slack message.")
+ .defaultValue("${" + CoreAttributes.FILENAME.key() + "}")
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+ .build();
+
+ public static final PropertyDescriptor FILE_MIME_TYPE = new PropertyDescriptor.Builder()
+ .name("file-mime-type")
+ .displayName("File Mime Type")
+ .description("Mime type of the file to be uploaded." +
+ " The property value will only be used if 'Upload FlowFile' has been set to 'Yes'." +
+ " If the property evaluated to null or empty string, then the mime type will be set to 'application/octet-stream' in the Slack message.")
+ .defaultValue("${" + CoreAttributes.MIME_TYPE.key() + "}")
+ .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+ .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+ .build();
+
+ public static final Relationship REL_SUCCESS = new Relationship.Builder()
+ .name("success")
+ .description("FlowFiles are routed to success after being successfully sent to Slack")
+ .build();
+
+ public static final Relationship REL_FAILURE = new Relationship.Builder()
+ .name("failure")
+ .description("FlowFiles are routed to failure if unable to be sent to Slack")
+ .build();
+
+ public static final List<PropertyDescriptor> properties = Collections.unmodifiableList(
+ Arrays.asList(POST_MESSAGE_URL, FILE_UPLOAD_URL, ACCESS_TOKEN, CHANNEL, TEXT, UPLOAD_FLOWFILE, FILE_TITLE, FILE_NAME, FILE_MIME_TYPE));
+
+ public static final Set<Relationship> relationships = Collections.unmodifiableSet(
+ new HashSet<>(Arrays.asList(REL_SUCCESS, REL_FAILURE)));
+
+ private final SortedSet<PropertyDescriptor> attachmentProperties = Collections.synchronizedSortedSet(new TreeSet<>());
+
+ private volatile PoolingHttpClientConnectionManager connManager;
+ private volatile CloseableHttpClient client;
+
+ private static final ContentType MIME_TYPE_PLAINTEXT_UTF8 = ContentType.create("text/plain", Charset.forName("UTF-8"));
+
+ @Override
+ protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(String propertyDescriptorName) {
+ return new PropertyDescriptor.Builder()
+ .name(propertyDescriptorName)
+ .description("Slack Attachment JSON snippet that will be added to the message. The property value will only be used if 'Upload FlowFile' has been set to 'No'." +
+ " If the property evaluated to null or empty string, or contains invalid JSON, then it will not be added to the Slack message.")
+ .required(false)
+ .addValidator(StandardValidators.createAttributeExpressionLanguageValidator(AttributeExpression.ResultType.STRING, true))
+ .addValidator(StandardValidators.ATTRIBUTE_KEY_PROPERTY_NAME_VALIDATOR)
+ .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+ .dynamic(true)
+ .build();
+ }
+
+ @Override
+ public List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+ return properties;
+ }
+
+ @Override
+ public Set<Relationship> getRelationships() {
+ return relationships;
+ }
+
+ @OnScheduled
+ public void initDynamicProperties(ProcessContext context) {
+ attachmentProperties.clear();
+ attachmentProperties.addAll(
+ context.getProperties().keySet()
+ .stream()
+ .filter(PropertyDescriptor::isDynamic)
+ .collect(Collectors.toList()));
+ }
+
+ @OnScheduled
+ public void initHttpResources() {
+ connManager = new PoolingHttpClientConnectionManager();
+
+ client = HttpClientBuilder.create()
+ .setConnectionManager(connManager)
+ .build();
+ }
+
+ @OnStopped
+ public void closeHttpResources() {
+ try {
+ if (client != null) {
+ client.close();
+ client = null;
+ }
+ if (connManager != null) {
+ connManager.close();
+ connManager = null;
+ }
+ } catch (IOException e) {
+ getLogger().error("Could not properly close HTTP connections.", e);
+ }
+ }
+
+ @Override
+ protected Collection<ValidationResult> customValidate(ValidationContext validationContext) {
+ List<ValidationResult> validationResults = new ArrayList<>();
+
+ boolean textSpecified = validationContext.getProperty(TEXT).isSet();
+ boolean attachmentSpecified = validationContext.getProperties().keySet()
+ .stream()
+ .anyMatch(PropertyDescriptor::isDynamic);
+ boolean uploadFileYes = validationContext.getProperty(UPLOAD_FLOWFILE).asBoolean();
+
+ if (!textSpecified && !attachmentSpecified && !uploadFileYes) {
+ validationResults.add(new ValidationResult.Builder()
+ .subject(TEXT.getDisplayName())
+ .valid(false)
+ .explanation("it is required if no attachment has been specified, nor 'Upload FlowFile' has been set to 'Yes'.")
+ .build());
+ }
+
+ return validationResults;
+ }
+
+ @Override
+ public void onTrigger(ProcessContext context, ProcessSession session) throws ProcessException {
+ FlowFile flowFile = session.get();
+ if (flowFile == null) {
+ return;
+ }
+
+ CloseableHttpResponse response = null;
+ try {
+ String url;
+ String contentType;
+ HttpEntity requestBody;
+
+ if (!context.getProperty(UPLOAD_FLOWFILE).asBoolean()) {
+ url = context.getProperty(POST_MESSAGE_URL).getValue();
+ contentType = ContentType.APPLICATION_JSON.toString();
+ requestBody = createTextMessageRequestBody(context, flowFile);
+ } else {
+ url = context.getProperty(FILE_UPLOAD_URL).getValue();
+ contentType = null; // it will be set implicitly by HttpClient in case of multipart post request
+ requestBody = createFileMessageRequestBody(context, session, flowFile);
+ }
+
+ HttpPost request = new HttpPost(url);
+ request.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + context.getProperty(ACCESS_TOKEN).getValue());
+ if (contentType != null) {
+ request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
+ }
+ request.setEntity(requestBody);
+
+ response = client.execute(request);
+
+ int statusCode = response.getStatusLine().getStatusCode();
+ getLogger().debug("Status code: " + statusCode);
+
+ if (!(statusCode >= 200 && statusCode < 300)) {
+ throw new PostSlackException("HTTP error code: " + statusCode);
+ }
+
+ JsonObject responseJson;
+ try {
+ responseJson = Json.createReader(response.getEntity().getContent()).readObject();
+ } catch (JsonParsingException e) {
+ throw new PostSlackException("Slack response JSON cannot be parsed.", e);
+ }
+
+ getLogger().debug("Slack response: " + responseJson.toString());
+
+ try {
+ if (!responseJson.getBoolean("ok")) {
+ throw new PostSlackException("Slack error response: " + responseJson.getString("error"));
+ }
+ } catch (NullPointerException | ClassCastException e) {
+ throw new PostSlackException("Slack response JSON does not contain 'ok' key or it has invalid value.", e);
+ }
+
+ JsonString warning = responseJson.getJsonString("warning");
+ if (warning != null) {
+ getLogger().warn("Slack warning message: " + warning.getString());
+ }
+
+ if (context.getProperty(UPLOAD_FLOWFILE).asBoolean()) {
+ JsonObject file = responseJson.getJsonObject("file");
+ if (file != null) {
+ JsonString fileUrl = file.getJsonString("url_private");
+ if (fileUrl != null) {
+ session.putAttribute(flowFile, "slack.file.url", fileUrl.getString());
+ }
+ }
+ }
+
+ session.transfer(flowFile, REL_SUCCESS);
+ session.getProvenanceReporter().send(flowFile, url);
+
+ } catch (IOException | PostSlackException e) {
+ getLogger().error("Failed to send message to Slack.", e);
+ flowFile = session.penalize(flowFile);
+ session.transfer(flowFile, REL_FAILURE);
+ context.yield();
+ } finally {
+ if (response != null) {
+ try {
+ // consume the entire content of the response (entity)
+ // so that the manager can release the connection back to the pool
+ EntityUtils.consume(response.getEntity());
+ response.close();
+ } catch (IOException e) {
+ getLogger().error("Could not properly close HTTP response.", e);
+ }
+ }
+ }
+ }
+
+ private HttpEntity createTextMessageRequestBody(ProcessContext context, FlowFile flowFile) throws PostSlackException, UnsupportedEncodingException {
+ JsonObjectBuilder jsonBuilder = Json.createObjectBuilder();
+
+ String channel = context.getProperty(CHANNEL).evaluateAttributeExpressions(flowFile).getValue();
+ if (channel == null || channel.isEmpty()) {
+ throw new PostSlackException("The channel must be specified.");
+ }
+ jsonBuilder.add("channel", channel);
+
+ String text = context.getProperty(TEXT).evaluateAttributeExpressions(flowFile).getValue();
+ if (text != null && !text.isEmpty()){
+ jsonBuilder.add("text", text);
+ } else {
+ if (attachmentProperties.isEmpty()) {
+ throw new PostSlackException("The text of the message must be specified if no attachment has been specified and 'Upload File' has been set to 'No'.");
+ }
+ }
+
+ if (!attachmentProperties.isEmpty()) {
+ JsonArrayBuilder jsonArrayBuilder = Json.createArrayBuilder();
+ for (PropertyDescriptor attachmentProperty : attachmentProperties) {
+ String propertyValue = context.getProperty(attachmentProperty).evaluateAttributeExpressions(flowFile).getValue();
+ if (propertyValue != null && !propertyValue.isEmpty()) {
+ try {
+ jsonArrayBuilder.add(Json.createReader(new StringReader(propertyValue)).readObject());
+ } catch (JsonParsingException e) {
+ getLogger().warn(attachmentProperty.getName() + " property contains no valid JSON, has been skipped.");
+ }
+ } else {
+ getLogger().warn(attachmentProperty.getName() + " property has no value, has been skipped.");
+ }
+ }
+ jsonBuilder.add("attachments", jsonArrayBuilder);
+ }
+
+ return new StringEntity(jsonBuilder.build().toString(), Charset.forName("UTF-8"));
+ }
+
+ private HttpEntity createFileMessageRequestBody(ProcessContext context, ProcessSession session, FlowFile flowFile) throws PostSlackException {
+ MultipartEntityBuilder multipartBuilder = MultipartEntityBuilder.create();
+
+ String channel = context.getProperty(CHANNEL).evaluateAttributeExpressions(flowFile).getValue();
+ if (channel == null || channel.isEmpty()) {
+ throw new PostSlackException("The channel must be specified.");
+ }
+ multipartBuilder.addTextBody("channels", channel, MIME_TYPE_PLAINTEXT_UTF8);
+
+ String text = context.getProperty(TEXT).evaluateAttributeExpressions(flowFile).getValue();
+ if (text != null && !text.isEmpty()) {
+ multipartBuilder.addTextBody("initial_comment", text, MIME_TYPE_PLAINTEXT_UTF8);
+ }
+
+ String title = context.getProperty(FILE_TITLE).evaluateAttributeExpressions(flowFile).getValue();
+ if (title != null && !title.isEmpty()) {
+ multipartBuilder.addTextBody("title", title, MIME_TYPE_PLAINTEXT_UTF8);
+ }
+
+ String fileName = context.getProperty(FILE_NAME).evaluateAttributeExpressions(flowFile).getValue();
+ if (fileName == null || fileName.isEmpty()) {
+ fileName = "file";
+ getLogger().warn("File name not specified, has been set to {}.", new Object[]{ fileName });
+ }
+ multipartBuilder.addTextBody("filename", fileName, MIME_TYPE_PLAINTEXT_UTF8);
+
+ ContentType mimeType;
+ String mimeTypeStr = context.getProperty(FILE_MIME_TYPE).evaluateAttributeExpressions(flowFile).getValue();
+ if (mimeTypeStr == null || mimeTypeStr.isEmpty()) {
+ mimeType = ContentType.APPLICATION_OCTET_STREAM;
+ getLogger().warn("Mime type not specified, has been set to {}.", new Object[]{ mimeType.getMimeType() });
+ } else {
+ mimeType = ContentType.getByMimeType(mimeTypeStr);
+ if (mimeType == null) {
+ mimeType = ContentType.APPLICATION_OCTET_STREAM;
+ getLogger().warn("Unknown mime type specified ({}), has been set to {}.", new Object[]{ mimeTypeStr, mimeType.getMimeType() });
+ }
+ }
+
+ multipartBuilder.addBinaryBody("file", session.read(flowFile), mimeType, fileName);
+
+ return multipartBuilder.build();
+ }
+
+ private static class PostSlackException extends Exception {
+ PostSlackException(String message) {
+ super(message);
+ }
+
+ PostSlackException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+}
diff --git a/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
index 9a861ee..47bc1d4 100644
--- a/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
+++ b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
@@ -12,4 +12,5 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-org.apache.nifi.processors.slack.PutSlack
\ No newline at end of file
+org.apache.nifi.processors.slack.PutSlack
+org.apache.nifi.processors.slack.PostSlack
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/main/resources/docs/org.apache.nifi.processors.slack.PostSlack/additionalDetails.html b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/main/resources/docs/org.apache.nifi.processors.slack.PostSlack/additionalDetails.html
new file mode 100644
index 0000000..ea85706
--- /dev/null
+++ b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/main/resources/docs/org.apache.nifi.processors.slack.PostSlack/additionalDetails.html
@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<html lang="en">
+ <!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+ <head>
+ <meta charset="utf-8" />
+ <title>PostSlack</title>
+ <link rel="stylesheet" href="../../../../../css/component-usage.css" type="text/css" />
+ <style>
+ dt {font-style: italic;}
+ </style>
+ </head>
+
+ <body>
+ <!-- Processor Documentation ================================================== -->
+ <h2>Description:</h2>
+ <p>
+ The PostSlack processor sends messages on Slack, a team-oriented messaging service.
+ The message can be a simple text message, furthermore Slack attachments can also be specified
+ or the FlowFile content (e.g. an image) can be uploaded and attached to the message too.
+ </p>
+ <h3>Slack App setup</h3>
+ <p>
+ This processor uses Slack Web API methods to post messages to a specific channel.
+ Before using PostSlack, you need to create a Slack App, to add a Bot User to the app,
+ and to install the app in your Slack workspace. After the app installed, you can get
+ the Bot User's OAuth Access Token that will be needed to authenticate and authorize
+ your PostSlack processor to Slack.
+ </p>
+ <h3>Message types</h3>
+ <p>
+ You can send the following types of messages with PostSlack:
+ </p>
+ <dl>
+ <dt>Text-only message</dt>
+ <dd>
+ A simple text message. In this case you must specify the <Text> property,
+ while <Upload FlowFile> is 'No' and no attachments defined through dynamic properties.
+ File related properties will be ignored in this case.
+ </dd>
+ <dt>Text message with attachment</dt>
+ <dd>
+ Besides the <Text> property, one or more attachments are also defined through dynamic properties
+ (for more details see the <a href="#slack-attachments">Slack attachments</a> section below).
+ The <Upload FlowFile> property needs to be set to 'No'. File related properties will be ignored.
+ </dd>
+ <dt>Attachment-only message</dt>
+ <dd>
+ The same as the previous one, but the <Text> property is not specified
+ (so no text section will be displayed at the beginning of the message, only the attachment(s)).
+ </dd>
+ <dt>Text message with file upload</dt>
+ <dd>
+ You need to specify the <Text> property and set <Upload FlowFile> to 'Yes'.
+ The content of the FlowFile will be uploaded with the message and it will be displayed
+ below the text. You should specify <File Name> and <File Mime Type> properties too
+ (otherwise some fallback values will be set), and optionally <File Title>.
+ The dynamic properties will be ignored in this case.
+ </dd>
+ <dt>File upload message without text</dt>
+ <dd>
+ The same as the previous one, but the <Text> property is not specified
+ (so no text section will be displayed at the beginning of the message, only the uploaded file).
+ </dd>
+ </dl>
+ <h3 id="slack-attachments">Slack attachments</h3>
+ <p>
+ Slack content and link attachments can be added to the message through dynamic properties.
+ Please note that this kind of Slack message attachments does not involve the file
+ upload itself, but rather contain links to external resources (or internal resources already uploaded
+ to Slack). Please also note that this functionality does not work together with file upload
+ (so it can only be used when 'Upload FlowFile' has been set to 'No').
+ </p>
+ <p>
+ The Dynamic Properties can be used to specify these Slack message attachments as JSON snippets.
+ Each property value will be converted to JSON and will be added to the array of attachments in the JSON
+ payload being sent to Slack.
+ </p>
+ <p>
+ Example JSON snippets to define Slack attachments:
+ </p>
+ <pre>
+ {
+ "text": "Text that appears within the attachment",
+ "image_url": "http://some-website.com/path/to/image.jpg"
+ }
+ </pre>
+ <pre>
+ {
+ "title": "Title of the attachment",
+ "image_url": "http://some-website.com/path/to/image.jpg"
+ }
+ </pre>
+ </body>
+</html>
diff --git a/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/test/java/org/apache/nifi/processors/slack/PostSlackCaptureServlet.java b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/test/java/org/apache/nifi/processors/slack/PostSlackCaptureServlet.java
new file mode 100644
index 0000000..629d78a
--- /dev/null
+++ b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/test/java/org/apache/nifi/processors/slack/PostSlackCaptureServlet.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.processors.slack;
+
+import org.apache.http.HttpStatus;
+import org.apache.http.entity.ContentType;
+import org.apache.nifi.stream.io.StreamUtils;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+public class PostSlackCaptureServlet extends HttpServlet {
+
+ static final String REQUEST_PATH_SUCCESS_TEXT_MSG = "/success/text_msg";
+ static final String REQUEST_PATH_SUCCESS_FILE_MSG = "/success/file_msg";
+ static final String REQUEST_PATH_WARNING = "/warning";
+ static final String REQUEST_PATH_ERROR = "/error";
+ static final String REQUEST_PATH_EMPTY_JSON = "/empty-json";
+ static final String REQUEST_PATH_INVALID_JSON = "/invalid-json";
+
+ private static final String RESPONSE_SUCCESS_TEXT_MSG = "{\"ok\": true}";
+ private static final String RESPONSE_SUCCESS_FILE_MSG = "{\"ok\": true, \"file\": {\"url_private\": \"slack-file-url\"}}";
+ private static final String RESPONSE_WARNING = "{\"ok\": true, \"warning\": \"slack-warning\"}";
+ private static final String RESPONSE_ERROR = "{\"ok\": false, \"error\": \"slack-error\"}";
+ private static final String RESPONSE_EMPTY_JSON = "{}";
+ private static final String RESPONSE_INVALID_JSON = "{invalid-json}";
+
+ private static final Map<String, String> RESPONSE_MAP;
+
+ static {
+ RESPONSE_MAP = new HashMap<>();
+
+ RESPONSE_MAP.put(REQUEST_PATH_SUCCESS_TEXT_MSG, RESPONSE_SUCCESS_TEXT_MSG);
+ RESPONSE_MAP.put(REQUEST_PATH_SUCCESS_FILE_MSG, RESPONSE_SUCCESS_FILE_MSG);
+ RESPONSE_MAP.put(REQUEST_PATH_WARNING, RESPONSE_WARNING);
+ RESPONSE_MAP.put(REQUEST_PATH_ERROR, RESPONSE_ERROR);
+ RESPONSE_MAP.put(REQUEST_PATH_EMPTY_JSON, RESPONSE_EMPTY_JSON);
+ RESPONSE_MAP.put(REQUEST_PATH_INVALID_JSON, RESPONSE_INVALID_JSON);
+ }
+
+ private volatile boolean interacted;
+ private volatile Map<String, String> lastPostHeaders;
+ private volatile byte[] lastPostBody;
+
+ public Map<String, String> getLastPostHeaders() {
+ return lastPostHeaders;
+ }
+
+ public byte[] getLastPostBody() {
+ return lastPostBody;
+ }
+
+ public boolean hasBeenInteracted() {
+ return interacted;
+ }
+
+ @Override
+ protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws IOException {
+ interacted = true;
+
+ Enumeration<String> headerNames = request.getHeaderNames();
+ lastPostHeaders = new HashMap<>();
+ while (headerNames.hasMoreElements()) {
+ String headerName = headerNames.nextElement();
+ lastPostHeaders.put(headerName, request.getHeader(headerName));
+ }
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ StreamUtils.copy(request.getInputStream(), baos);
+ lastPostBody = baos.toByteArray();
+
+ String responseJson = RESPONSE_MAP.get(request.getPathInfo());
+ if (responseJson != null) {
+ response.setContentType(ContentType.APPLICATION_JSON.toString());
+ PrintWriter out = response.getWriter();
+ out.print(responseJson);
+ out.close();
+ } else {
+ response.setStatus(HttpStatus.SC_BAD_REQUEST);
+ }
+ }
+}
diff --git a/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/test/java/org/apache/nifi/processors/slack/PostSlackConfigValidationTest.java b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/test/java/org/apache/nifi/processors/slack/PostSlackConfigValidationTest.java
new file mode 100644
index 0000000..6211b0d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/test/java/org/apache/nifi/processors/slack/PostSlackConfigValidationTest.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.processors.slack;
+
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.junit.Before;
+import org.junit.Test;
+
+public class PostSlackConfigValidationTest {
+
+ private TestRunner testRunner;
+
+ @Before
+ public void setup() {
+ testRunner = TestRunners.newTestRunner(PostSlack.class);
+ }
+
+ @Test
+ public void validationShouldPassIfTheConfigIsFine() {
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+
+ testRunner.assertValid();
+ }
+
+ @Test
+ public void validationShouldFailIfPostMessageUrlIsEmptyString() {
+ testRunner.setProperty(PostSlack.POST_MESSAGE_URL, "");
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+
+ testRunner.assertNotValid();
+ }
+
+ @Test
+ public void validationShouldFailIfPostMessageUrlIsNotValid() {
+ testRunner.setProperty(PostSlack.POST_MESSAGE_URL, "not-url");
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+
+ testRunner.assertNotValid();
+ }
+
+ @Test
+ public void validationShouldFailIfFileUploadUrlIsEmptyString() {
+ testRunner.setProperty(PostSlack.FILE_UPLOAD_URL, "");
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+
+ testRunner.assertNotValid();
+ }
+
+ @Test
+ public void validationShouldFailIfFileUploadUrlIsNotValid() {
+ testRunner.setProperty(PostSlack.FILE_UPLOAD_URL, "not-url");
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+
+ testRunner.assertNotValid();
+ }
+
+ @Test
+ public void validationShouldFailIfAccessTokenIsNotGiven() {
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+
+ testRunner.assertNotValid();
+ }
+
+ @Test
+ public void validationShouldFailIfAccessTokenIsEmptyString() {
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+
+ testRunner.assertNotValid();
+ }
+
+ @Test
+ public void validationShouldFailIfChannelIsNotGiven() {
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+
+ testRunner.assertNotValid();
+ }
+
+ @Test
+ public void validationShouldFailIfChannelIsEmptyString() {
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+
+ testRunner.assertNotValid();
+ }
+
+ @Test
+ public void validationShouldFailIfTextIsNotGivenAndNoAttachmentSpecifiedNorFileUploadChosen() {
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+
+ testRunner.assertNotValid();
+ }
+
+ @Test
+ public void validationShouldPassIfTextIsNotGivenButAttachmentSpecified() {
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty("attachment_01", "{\"my-attachment-key\": \"my-attachment-value\"}");
+
+ testRunner.assertValid();
+ }
+
+ @Test
+ public void validationShouldPassIfTextIsNotGivenButFileUploadChosen() {
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.UPLOAD_FLOWFILE, PostSlack.UPLOAD_FLOWFILE_YES);
+
+ testRunner.assertValid();
+ }
+
+ @Test
+ public void validationShouldFailIfTextIsEmptyString() {
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "");
+ testRunner.setProperty("attachment_01", "{\"my-attachment-key\": \"my-attachment-value\"}");
+ testRunner.setProperty(PostSlack.UPLOAD_FLOWFILE, PostSlack.UPLOAD_FLOWFILE_YES);
+
+ testRunner.assertNotValid();
+ }
+}
diff --git a/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/test/java/org/apache/nifi/processors/slack/PostSlackFileMessageTest.java b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/test/java/org/apache/nifi/processors/slack/PostSlackFileMessageTest.java
new file mode 100644
index 0000000..d4d7ea2
--- /dev/null
+++ b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/test/java/org/apache/nifi/processors/slack/PostSlackFileMessageTest.java
@@ -0,0 +1,312 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.processors.slack;
+
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.flowfile.attributes.CoreAttributes;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.apache.nifi.web.util.TestServer;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.apache.nifi.processors.slack.PostSlackCaptureServlet.REQUEST_PATH_SUCCESS_FILE_MSG;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class PostSlackFileMessageTest {
+
+ private TestRunner testRunner;
+
+ private TestServer server;
+ private PostSlackCaptureServlet servlet;
+
+ @Before
+ public void setup() throws Exception {
+ testRunner = TestRunners.newTestRunner(PostSlack.class);
+
+ servlet = new PostSlackCaptureServlet();
+
+ ServletContextHandler handler = new ServletContextHandler();
+ handler.addServlet(new ServletHolder(servlet), "/*");
+
+ server = new TestServer();
+ server.addHandler(handler);
+ server.startServer();
+ }
+
+ @Test
+ public void sendMessageWithBasicPropertiesSuccessfully() {
+ testRunner.setProperty(PostSlack.FILE_UPLOAD_URL, server.getUrl() + REQUEST_PATH_SUCCESS_FILE_MSG);
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.UPLOAD_FLOWFILE, PostSlack.UPLOAD_FLOWFILE_YES);
+
+ Map<String, String> flowFileAttributes = new HashMap<>();
+ flowFileAttributes.put(CoreAttributes.FILENAME.key(), "my-file-name");
+ flowFileAttributes.put(CoreAttributes.MIME_TYPE.key(), "image/png");
+
+ // in order not to make the assertion logic (even more) complicated, the file content is tested with character data instead of binary data
+ testRunner.enqueue("my-data", flowFileAttributes);
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_SUCCESS);
+
+ assertRequest("my-file-name", "image/png", null, null);
+
+ FlowFile flowFileOut = testRunner.getFlowFilesForRelationship(PutSlack.REL_SUCCESS).get(0);
+ assertEquals("slack-file-url", flowFileOut.getAttribute("slack.file.url"));
+ }
+
+ @Test
+ public void sendMessageWithAllPropertiesSuccessfully() {
+ testRunner.setProperty(PostSlack.FILE_UPLOAD_URL, server.getUrl() + REQUEST_PATH_SUCCESS_FILE_MSG);
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+ testRunner.setProperty(PostSlack.UPLOAD_FLOWFILE, PostSlack.UPLOAD_FLOWFILE_YES);
+ testRunner.setProperty(PostSlack.FILE_TITLE, "my-file-title");
+ testRunner.setProperty(PostSlack.FILE_NAME, "my-file-name");
+ testRunner.setProperty(PostSlack.FILE_MIME_TYPE, "image/png");
+
+ testRunner.enqueue("my-data");
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_SUCCESS);
+
+ assertRequest("my-file-name", "image/png", "my-text", "my-file-title");
+
+ FlowFile flowFileOut = testRunner.getFlowFilesForRelationship(PutSlack.REL_SUCCESS).get(0);
+ assertEquals("slack-file-url", flowFileOut.getAttribute("slack.file.url"));
+ }
+
+ @Test
+ public void processShouldFailWhenChannelIsEmpty() {
+ testRunner.setProperty(PostSlack.FILE_UPLOAD_URL, server.getUrl());
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "${dummy}");
+ testRunner.setProperty(PostSlack.UPLOAD_FLOWFILE, PostSlack.UPLOAD_FLOWFILE_YES);
+
+ testRunner.enqueue("my-data");
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_FAILURE);
+
+ assertFalse(servlet.hasBeenInteracted());
+ }
+
+ @Test
+ public void fileNameShouldHaveFallbackValueWhenEmpty() {
+ testRunner.setProperty(PostSlack.FILE_UPLOAD_URL, server.getUrl() + REQUEST_PATH_SUCCESS_FILE_MSG);
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.UPLOAD_FLOWFILE, PostSlack.UPLOAD_FLOWFILE_YES);
+ testRunner.setProperty(PostSlack.FILE_NAME, "${dummy}");
+ testRunner.setProperty(PostSlack.FILE_MIME_TYPE, "image/png");
+
+ testRunner.enqueue("my-data");
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_SUCCESS);
+
+ // fallback value for file name is 'file'
+ assertRequest("file", "image/png", null, null);
+
+ FlowFile flowFileOut = testRunner.getFlowFilesForRelationship(PutSlack.REL_SUCCESS).get(0);
+ assertEquals("slack-file-url", flowFileOut.getAttribute("slack.file.url"));
+ }
+
+ @Test
+ public void mimeTypeShouldHaveFallbackValueWhenEmpty() {
+ testRunner.setProperty(PostSlack.FILE_UPLOAD_URL, server.getUrl() + REQUEST_PATH_SUCCESS_FILE_MSG);
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.UPLOAD_FLOWFILE, PostSlack.UPLOAD_FLOWFILE_YES);
+ testRunner.setProperty(PostSlack.FILE_NAME, "my-file-name");
+ testRunner.setProperty(PostSlack.FILE_MIME_TYPE, "${dummy}");
+
+ testRunner.enqueue("my-data");
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_SUCCESS);
+
+ // fallback value for mime type is 'application/octet-stream'
+ assertRequest("my-file-name", "application/octet-stream", null, null);
+
+ FlowFile flowFileOut = testRunner.getFlowFilesForRelationship(PutSlack.REL_SUCCESS).get(0);
+ assertEquals("slack-file-url", flowFileOut.getAttribute("slack.file.url"));
+ }
+
+ @Test
+ public void mimeTypeShouldHaveFallbackValueWhenInvalid() {
+ testRunner.setProperty(PostSlack.FILE_UPLOAD_URL, server.getUrl() + REQUEST_PATH_SUCCESS_FILE_MSG);
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.UPLOAD_FLOWFILE, PostSlack.UPLOAD_FLOWFILE_YES);
+ testRunner.setProperty(PostSlack.FILE_NAME, "my-file-name");
+ testRunner.setProperty(PostSlack.FILE_MIME_TYPE, "invalid");
+
+ testRunner.enqueue("my-data");
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_SUCCESS);
+
+ // fallback value for mime type is 'application/octet-stream'
+ assertRequest("my-file-name", "application/octet-stream", null, null);
+
+ FlowFile flowFileOut = testRunner.getFlowFilesForRelationship(PutSlack.REL_SUCCESS).get(0);
+ assertEquals("slack-file-url", flowFileOut.getAttribute("slack.file.url"));
+ }
+
+ @Test
+ public void sendInternationalMessageSuccessfully() {
+ testRunner.setProperty(PostSlack.FILE_UPLOAD_URL, server.getUrl() + REQUEST_PATH_SUCCESS_FILE_MSG);
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "Iñtërnâtiônàližætiøn");
+ testRunner.setProperty(PostSlack.UPLOAD_FLOWFILE, PostSlack.UPLOAD_FLOWFILE_YES);
+ testRunner.setProperty(PostSlack.FILE_TITLE, "Iñtërnâtiônàližætiøn");
+
+ testRunner.enqueue(new byte[0]);
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_SUCCESS);
+
+ Map<String, String> parts = parsePostBodyParts(parseMultipartBoundary(servlet.getLastPostHeaders().get("Content-Type")));
+ assertEquals("Iñtërnâtiônàližætiøn", parts.get("initial_comment"));
+ assertEquals("Iñtërnâtiônàližætiøn", parts.get("title"));
+
+ FlowFile flowFileOut = testRunner.getFlowFilesForRelationship(PutSlack.REL_SUCCESS).get(0);
+ assertEquals("slack-file-url", flowFileOut.getAttribute("slack.file.url"));
+ }
+
+ private void assertRequest(String fileName, String mimeType, String text, String title) {
+ Map<String, String> requestHeaders = servlet.getLastPostHeaders();
+ assertEquals("Bearer my-access-token", requestHeaders.get("Authorization"));
+
+ String contentType = requestHeaders.get("Content-Type");
+ assertTrue(contentType.startsWith("multipart/form-data"));
+
+ String boundary = parseMultipartBoundary(contentType);
+ assertNotNull("Multipart boundary not found in Content-Type header: " + contentType, boundary);
+
+ Map<String, String> parts = parsePostBodyParts(boundary);
+
+ assertNotNull("'channels' parameter not found in the POST request body", parts.get("channels"));
+ assertEquals("'channels' parameter has wrong value", "my-channel", parts.get("channels"));
+
+ if (text != null) {
+ assertNotNull("'initial_comment' parameter not found in the POST request body", parts.get("initial_comment"));
+ assertEquals("'initial_comment' parameter has wrong value", text, parts.get("initial_comment"));
+ }
+
+ assertNotNull("'filename' parameter not found in the POST request body", parts.get("filename"));
+ assertEquals("'fileName' parameter has wrong value", fileName, parts.get("filename"));
+
+ if (title != null) {
+ assertNotNull("'title' parameter not found in the POST request body", parts.get("title"));
+ assertEquals("'title' parameter has wrong value", title, parts.get("title"));
+ }
+
+ assertNotNull("The file part not found in the POST request body", parts.get("file"));
+
+ Map<String, String> fileParameters = parseFilePart(boundary);
+ assertEquals("File data is wrong in the POST request body", "my-data", fileParameters.get("data"));
+ assertEquals("'filename' attribute of the file part has wrong value", fileName, fileParameters.get("filename"));
+ assertEquals("Content-Type of the file part is wrong", mimeType, fileParameters.get("contentType"));
+ }
+
+ private String parseMultipartBoundary(String contentType) {
+ String boundary = null;
+
+ Pattern boundaryPattern = Pattern.compile("boundary=(.*?)$");
+ Matcher boundaryMatcher = boundaryPattern.matcher(contentType);
+
+ if (boundaryMatcher.find()) {
+ boundary = "--" + boundaryMatcher.group(1);
+ }
+
+ return boundary;
+ }
+
+ private Map<String, String> parsePostBodyParts(String boundary) {
+ Pattern partNamePattern = Pattern.compile("name=\"(.*?)\"");
+ Pattern partDataPattern = Pattern.compile("\r\n\r\n(.*?)\r\n$");
+
+ String[] postBodyParts = new String(servlet.getLastPostBody(), Charset.forName("UTF-8")).split(boundary);
+
+ Map<String, String> parts = new HashMap<>();
+
+ for (String part: postBodyParts) {
+ Matcher partNameMatcher = partNamePattern.matcher(part);
+ Matcher partDataMatcher = partDataPattern.matcher(part);
+
+ if (partNameMatcher.find() && partDataMatcher.find()) {
+ String partName = partNameMatcher.group(1);
+ String partData = partDataMatcher.group(1);
+
+ parts.put(partName, partData);
+ }
+ }
+
+ return parts;
+ }
+
+ private Map<String, String> parseFilePart(String boundary) {
+ Pattern partNamePattern = Pattern.compile("name=\"file\"");
+ Pattern partDataPattern = Pattern.compile("\r\n\r\n(.*?)\r\n$");
+ Pattern partFilenamePattern = Pattern.compile("filename=\"(.*?)\"");
+ Pattern partContentTypePattern = Pattern.compile("Content-Type: (.*?)\r\n");
+
+ String[] postBodyParts = new String(servlet.getLastPostBody(), Charset.forName("UTF-8")).split(boundary);
+
+ Map<String, String> fileParameters = new HashMap<>();
+
+ for (String part: postBodyParts) {
+ Matcher partNameMatcher = partNamePattern.matcher(part);
+
+ if (partNameMatcher.find()) {
+ Matcher partDataMatcher = partDataPattern.matcher(part);
+ if (partDataMatcher.find()) {
+ fileParameters.put("data", partDataMatcher.group(1));
+ }
+
+ Matcher partFilenameMatcher = partFilenamePattern.matcher(part);
+ if (partFilenameMatcher.find()) {
+ fileParameters.put("filename", partFilenameMatcher.group(1));
+ }
+
+ Matcher partContentTypeMatcher = partContentTypePattern.matcher(part);
+ if (partContentTypeMatcher.find()) {
+ fileParameters.put("contentType", partContentTypeMatcher.group(1));
+ }
+ }
+ }
+
+ return fileParameters;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/test/java/org/apache/nifi/processors/slack/PostSlackTextMessageTest.java b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/test/java/org/apache/nifi/processors/slack/PostSlackTextMessageTest.java
new file mode 100644
index 0000000..809d23c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-slack-bundle/nifi-slack-processors/src/test/java/org/apache/nifi/processors/slack/PostSlackTextMessageTest.java
@@ -0,0 +1,282 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.processors.slack;
+
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.apache.nifi.web.util.TestServer;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import java.io.ByteArrayInputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.util.Map;
+
+import static org.apache.nifi.processors.slack.PostSlackCaptureServlet.REQUEST_PATH_EMPTY_JSON;
+import static org.apache.nifi.processors.slack.PostSlackCaptureServlet.REQUEST_PATH_ERROR;
+import static org.apache.nifi.processors.slack.PostSlackCaptureServlet.REQUEST_PATH_INVALID_JSON;
+import static org.apache.nifi.processors.slack.PostSlackCaptureServlet.REQUEST_PATH_SUCCESS_TEXT_MSG;
+import static org.apache.nifi.processors.slack.PostSlackCaptureServlet.REQUEST_PATH_WARNING;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+public class PostSlackTextMessageTest {
+
+ private TestRunner testRunner;
+
+ private TestServer server;
+ private PostSlackCaptureServlet servlet;
+
+ @Before
+ public void setup() throws Exception {
+ testRunner = TestRunners.newTestRunner(PostSlack.class);
+
+ servlet = new PostSlackCaptureServlet();
+
+ ServletContextHandler handler = new ServletContextHandler();
+ handler.addServlet(new ServletHolder(servlet), "/*");
+
+ server = new TestServer();
+ server.addHandler(handler);
+ server.startServer();
+ }
+
+ @Test
+ public void sendTextOnlyMessageSuccessfully() {
+ testRunner.setProperty(PostSlack.POST_MESSAGE_URL, server.getUrl() + REQUEST_PATH_SUCCESS_TEXT_MSG);
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+
+ testRunner.enqueue(new byte[0]);
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_SUCCESS);
+
+ JsonObject requestBodyJson = getRequestBodyJson();
+ assertBasicRequest(requestBodyJson);
+ assertEquals("my-text", requestBodyJson.getString("text"));
+ }
+
+ @Test
+ public void sendTextWithAttachmentMessageSuccessfully() {
+ testRunner.setProperty(PostSlack.POST_MESSAGE_URL, server.getUrl() + REQUEST_PATH_SUCCESS_TEXT_MSG);
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+ testRunner.setProperty("attachment_01", "{\"my-attachment-key\": \"my-attachment-value\"}");
+
+ testRunner.enqueue(new byte[0]);
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_SUCCESS);
+
+ JsonObject requestBodyJson = getRequestBodyJson();
+ assertBasicRequest(requestBodyJson);
+ assertEquals("my-text", requestBodyJson.getString("text"));
+ assertEquals("[{\"my-attachment-key\":\"my-attachment-value\"}]", requestBodyJson.getJsonArray("attachments").toString());
+ }
+
+ @Test
+ public void sendAttachmentOnlyMessageSuccessfully() {
+ testRunner.setProperty(PostSlack.POST_MESSAGE_URL, server.getUrl() + REQUEST_PATH_SUCCESS_TEXT_MSG);
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty("attachment_01", "{\"my-attachment-key\": \"my-attachment-value\"}");
+
+ testRunner.enqueue(new byte[0]);
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_SUCCESS);
+
+ JsonObject requestBodyJson = getRequestBodyJson();
+ assertBasicRequest(requestBodyJson);
+ assertEquals("[{\"my-attachment-key\":\"my-attachment-value\"}]", requestBodyJson.getJsonArray("attachments").toString());
+ }
+
+ @Test
+ public void processShouldFailWhenChannelIsEmpty() {
+ testRunner.setProperty(PostSlack.POST_MESSAGE_URL, server.getUrl());
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "${dummy}");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+
+ testRunner.enqueue(new byte[0]);
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_FAILURE);
+
+ assertFalse(servlet.hasBeenInteracted());
+ }
+
+ @Test
+ public void processShouldFailWhenTextIsEmptyAndNoAttachmentSpecified() {
+ testRunner.setProperty(PostSlack.POST_MESSAGE_URL, server.getUrl());
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "${dummy}");
+
+ testRunner.enqueue(new byte[0]);
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_FAILURE);
+
+ assertFalse(servlet.hasBeenInteracted());
+ }
+
+ @Test
+ public void emptyAttachmentShouldBeSkipped() {
+ testRunner.setProperty(PostSlack.POST_MESSAGE_URL, server.getUrl() + REQUEST_PATH_SUCCESS_TEXT_MSG);
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty("attachment_01", "${dummy}");
+ testRunner.setProperty("attachment_02", "{\"my-attachment-key\": \"my-attachment-value\"}");
+
+ testRunner.enqueue(new byte[0]);
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_SUCCESS);
+
+ JsonObject requestBodyJson = getRequestBodyJson();
+ assertBasicRequest(requestBodyJson);
+ assertEquals(1, requestBodyJson.getJsonArray("attachments").size());
+ assertEquals("[{\"my-attachment-key\":\"my-attachment-value\"}]", requestBodyJson.getJsonArray("attachments").toString());
+ }
+
+ @Test
+ public void invalidAttachmentShouldBeSkipped() {
+ testRunner.setProperty(PostSlack.POST_MESSAGE_URL, server.getUrl() + REQUEST_PATH_SUCCESS_TEXT_MSG);
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty("attachment_01", "{invalid-json}");
+ testRunner.setProperty("attachment_02", "{\"my-attachment-key\": \"my-attachment-value\"}");
+
+ testRunner.enqueue(new byte[0]);
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_SUCCESS);
+
+ JsonObject requestBodyJson = getRequestBodyJson();
+ assertBasicRequest(requestBodyJson);
+ assertEquals(1, requestBodyJson.getJsonArray("attachments").size());
+ assertEquals("[{\"my-attachment-key\":\"my-attachment-value\"}]", requestBodyJson.getJsonArray("attachments").toString());
+ }
+
+ @Test
+ public void processShouldFailWhenHttpErrorCodeReturned() {
+ testRunner.setProperty(PostSlack.POST_MESSAGE_URL, server.getUrl());
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+
+ testRunner.enqueue(new byte[0]);
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_FAILURE);
+ }
+
+ @Test
+ public void processShouldFailWhenSlackReturnsError() {
+ testRunner.setProperty(PostSlack.POST_MESSAGE_URL, server.getUrl() + REQUEST_PATH_ERROR);
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+
+ testRunner.enqueue(new byte[0]);
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_FAILURE);
+ }
+
+ @Test
+ public void processShouldNotFailWhenSlackReturnsWarning() {
+ testRunner.setProperty(PostSlack.POST_MESSAGE_URL, server.getUrl() + REQUEST_PATH_WARNING);
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+
+ testRunner.enqueue(new byte[0]);
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_SUCCESS);
+
+ assertBasicRequest(getRequestBodyJson());
+ }
+
+ @Test
+ public void processShouldFailWhenSlackReturnsEmptyJson() {
+ testRunner.setProperty(PostSlack.POST_MESSAGE_URL, server.getUrl() + REQUEST_PATH_EMPTY_JSON);
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+
+ testRunner.enqueue(new byte[0]);
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_FAILURE);
+ }
+
+ @Test
+ public void processShouldFailWhenSlackReturnsInvalidJson() {
+ testRunner.setProperty(PostSlack.POST_MESSAGE_URL, server.getUrl() + REQUEST_PATH_INVALID_JSON);
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "my-text");
+
+ testRunner.enqueue(new byte[0]);
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_FAILURE);
+ }
+
+ @Test
+ public void sendInternationalMessageSuccessfully() {
+ testRunner.setProperty(PostSlack.POST_MESSAGE_URL, server.getUrl() + REQUEST_PATH_SUCCESS_TEXT_MSG);
+ testRunner.setProperty(PostSlack.ACCESS_TOKEN, "my-access-token");
+ testRunner.setProperty(PostSlack.CHANNEL, "my-channel");
+ testRunner.setProperty(PostSlack.TEXT, "Iñtërnâtiônàližætiøn");
+
+ testRunner.enqueue(new byte[0]);
+ testRunner.run(1);
+
+ testRunner.assertAllFlowFilesTransferred(PutSlack.REL_SUCCESS);
+
+ JsonObject requestBodyJson = getRequestBodyJson();
+ assertBasicRequest(requestBodyJson);
+ assertEquals("Iñtërnâtiônàližætiøn", requestBodyJson.getString("text"));
+ }
+
+ private void assertBasicRequest(JsonObject requestBodyJson) {
+ Map<String, String> requestHeaders = servlet.getLastPostHeaders();
+ assertEquals("Bearer my-access-token", requestHeaders.get("Authorization"));
+ assertEquals("application/json; charset=UTF-8", requestHeaders.get("Content-Type"));
+
+ assertEquals("my-channel", requestBodyJson.getString("channel"));
+ }
+
+ private JsonObject getRequestBodyJson() {
+ return Json.createReader(
+ new InputStreamReader(
+ new ByteArrayInputStream(servlet.getLastPostBody()), Charset.forName("UTF-8")))
+ .readObject();
+ }
+}