From 4548bdc1d9392969a3c39746b784fab8e25fcb43 Mon Sep 17 00:00:00 2001 From: Drew Baugher <46505179+dbbaughe@users.noreply.github.com> Date: Thu, 12 Aug 2021 17:47:12 -0700 Subject: [PATCH] Adds legacy chime, slack, custom webhook messages, request/response f (#53) * Adds legacy chime, slack, custom webhook messages, request/response for publishing legacy notifications, and method for executing transport action Signed-off-by: Drew Baugher <46505179+dbbaughe@users.noreply.github.com> * Fixes import and removes username/password that is not used by ISM Signed-off-by: Drew Baugher <46505179+dbbaughe@users.noreply.github.com> * Throws error for toXContent for legacy notification response Signed-off-by: Drew Baugher <46505179+dbbaughe@users.noreply.github.com> * Renames legacy destination types to have legacy prefix Signed-off-by: Drew Baugher <46505179+dbbaughe@users.noreply.github.com> * Obfuscates message to remove from logs in toString method Signed-off-by: Drew Baugher <46505179+dbbaughe@users.noreply.github.com> * Makes destinationt type private and updates places to use getter Signed-off-by: Drew Baugher <46505179+dbbaughe@users.noreply.github.com> * Inlines destination type Signed-off-by: Drew Baugher <46505179+dbbaughe@users.noreply.github.com> * Makes base message content final Signed-off-by: Drew Baugher <46505179+dbbaughe@users.noreply.github.com> * Requires url to be defined in LegacyCustomWebhookMessage for use across transport wire and only writes the full url instead of each individual part Signed-off-by: Drew Baugher <46505179+dbbaughe@users.noreply.github.com> --- .../message/LegacyBaseMessage.java | 128 ++++++++ .../message/LegacyChimeMessage.java | 91 ++++++ .../message/LegacyCustomWebhookMessage.java | 274 ++++++++++++++++++ .../message/LegacyDestinationType.java | 36 +++ .../message/LegacySlackMessage.java | 95 ++++++ .../response/LegacyBaseResponse.java | 60 ++++ .../response/LegacyDestinationResponse.java | 82 ++++++ .../NotificationsPluginInterface.kt | 28 ++ .../LegacyPublishNotificationRequest.kt | 82 ++++++ .../LegacyPublishNotificationResponse.kt | 63 ++++ .../action/NotificationsActions.kt | 13 + .../message/LegacyChimeMessageTest.java | 100 +++++++ .../LegacyCustomWebhookMessageTest.java | 177 +++++++++++ .../message/LegacySlackMessageTest.java | 110 +++++++ .../LegacyDestinationResponseTest.java | 82 ++++++ .../NotificationsPluginInterfaceTests.kt | 24 ++ .../LegacyPublishNotificationRequestTests.kt | 42 +++ .../LegacyPublishNotificationResponseTests.kt | 29 ++ 18 files changed, 1516 insertions(+) create mode 100644 src/main/java/org/opensearch/commons/destination/message/LegacyBaseMessage.java create mode 100644 src/main/java/org/opensearch/commons/destination/message/LegacyChimeMessage.java create mode 100644 src/main/java/org/opensearch/commons/destination/message/LegacyCustomWebhookMessage.java create mode 100644 src/main/java/org/opensearch/commons/destination/message/LegacyDestinationType.java create mode 100644 src/main/java/org/opensearch/commons/destination/message/LegacySlackMessage.java create mode 100644 src/main/java/org/opensearch/commons/destination/response/LegacyBaseResponse.java create mode 100644 src/main/java/org/opensearch/commons/destination/response/LegacyDestinationResponse.java create mode 100644 src/main/kotlin/org/opensearch/commons/notifications/action/LegacyPublishNotificationRequest.kt create mode 100644 src/main/kotlin/org/opensearch/commons/notifications/action/LegacyPublishNotificationResponse.kt create mode 100644 src/test/java/org/opensearch/commons/destination/message/LegacyChimeMessageTest.java create mode 100644 src/test/java/org/opensearch/commons/destination/message/LegacyCustomWebhookMessageTest.java create mode 100644 src/test/java/org/opensearch/commons/destination/message/LegacySlackMessageTest.java create mode 100644 src/test/java/org/opensearch/commons/destination/response/LegacyDestinationResponseTest.java create mode 100644 src/test/kotlin/org/opensearch/commons/notifications/action/LegacyPublishNotificationRequestTests.kt create mode 100644 src/test/kotlin/org/opensearch/commons/notifications/action/LegacyPublishNotificationResponseTests.kt diff --git a/src/main/java/org/opensearch/commons/destination/message/LegacyBaseMessage.java b/src/main/java/org/opensearch/commons/destination/message/LegacyBaseMessage.java new file mode 100644 index 00000000..b5122f31 --- /dev/null +++ b/src/main/java/org/opensearch/commons/destination/message/LegacyBaseMessage.java @@ -0,0 +1,128 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.opensearch.commons.destination.message; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; + +import org.apache.http.client.utils.URIBuilder; +import org.opensearch.common.Strings; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; +import org.opensearch.common.io.stream.Writeable; + +/** + * This class holds the generic parameters required for a + * message. + */ +public abstract class LegacyBaseMessage implements Writeable { + + private final LegacyDestinationType destinationType; + protected String destinationName; + protected String url; + private final String content; + + LegacyBaseMessage(final LegacyDestinationType destinationType, final String destinationName, final String content) { + if (destinationType == null) { + throw new IllegalArgumentException("Channel type must be defined"); + } + if (!Strings.hasLength(destinationName)) { + throw new IllegalArgumentException("Channel name must be defined"); + } + this.destinationType = destinationType; + this.destinationName = destinationName; + this.content = content; + } + + LegacyBaseMessage(final LegacyDestinationType destinationType, final String destinationName, final String content, final String url) { + this(destinationType, destinationName, content); + if (url == null) { + throw new IllegalArgumentException("url is invalid or empty"); + } + this.url = url; + } + + LegacyBaseMessage(StreamInput streamInput) throws IOException { + this.destinationType = streamInput.readEnum(LegacyDestinationType.class); + this.destinationName = streamInput.readString(); + this.url = streamInput.readOptionalString(); + this.content = streamInput.readString(); + } + + public void setUrl(String url) { + this.url = url; + } + + public LegacyDestinationType getChannelType() { + return destinationType; + } + + public String getChannelName() { + return destinationName; + } + + public String getMessageContent() { + return content; + } + + public String getUrl() { + return url; + } + + public URI getUri() { + return buildUri(getUrl().trim(), null, null, -1, null, null); + } + + protected URI buildUri(String endpoint, String scheme, String host, int port, String path, Map queryParams) { + try { + if (Strings.isNullOrEmpty(endpoint)) { + if (Strings.isNullOrEmpty(scheme)) { + scheme = "https"; + } + URIBuilder uriBuilder = new URIBuilder(); + if (queryParams != null) { + for (Map.Entry e : queryParams.entrySet()) + uriBuilder.addParameter(e.getKey(), e.getValue()); + } + return uriBuilder.setScheme(scheme).setHost(host).setPort(port).setPath(path).build(); + } + return new URIBuilder(endpoint).build(); + } catch (URISyntaxException exception) { + throw new IllegalStateException("Error creating URI"); + } + } + + @Override + public void writeTo(StreamOutput streamOutput) throws IOException { + streamOutput.writeEnum(destinationType); + streamOutput.writeString(destinationName); + streamOutput.writeOptionalString(url); + streamOutput.writeString(content); + } +} diff --git a/src/main/java/org/opensearch/commons/destination/message/LegacyChimeMessage.java b/src/main/java/org/opensearch/commons/destination/message/LegacyChimeMessage.java new file mode 100644 index 00000000..b457d9aa --- /dev/null +++ b/src/main/java/org/opensearch/commons/destination/message/LegacyChimeMessage.java @@ -0,0 +1,91 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.opensearch.commons.destination.message; + +import java.io.IOException; + +import org.opensearch.common.Strings; +import org.opensearch.common.io.stream.StreamInput; + +/** + * This class holds the contents of an Chime message + */ +public class LegacyChimeMessage extends LegacyBaseMessage { + private final String message; + + private LegacyChimeMessage(final String destinationName, final String url, final String message) { + super(LegacyDestinationType.LEGACY_CHIME, destinationName, message, url); + + if (Strings.isNullOrEmpty(message)) { + throw new IllegalArgumentException("Message content is missing"); + } + + this.message = message; + } + + public LegacyChimeMessage(StreamInput streamInput) throws IOException { + super(streamInput); + this.message = super.getMessageContent(); + } + + @Override + public String toString() { + return "DestinationType: " + getChannelType() + ", DestinationName:" + destinationName + ", Url: " + url + ", Message: <...>"; + } + + public static class Builder { + private String message; + private final String destinationName; + private String url; + + public Builder(String destinationName) { + this.destinationName = destinationName; + } + + public LegacyChimeMessage.Builder withMessage(String message) { + this.message = message; + return this; + } + + public LegacyChimeMessage.Builder withUrl(String url) { + this.url = url; + return this; + } + + public LegacyChimeMessage build() { + return new LegacyChimeMessage(this.destinationName, this.url, this.message); + } + } + + public String getMessage() { + return message; + } + + public String getUrl() { + return url; + } +} diff --git a/src/main/java/org/opensearch/commons/destination/message/LegacyCustomWebhookMessage.java b/src/main/java/org/opensearch/commons/destination/message/LegacyCustomWebhookMessage.java new file mode 100644 index 00000000..9645b327 --- /dev/null +++ b/src/main/java/org/opensearch/commons/destination/message/LegacyCustomWebhookMessage.java @@ -0,0 +1,274 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.opensearch.commons.destination.message; + +import java.io.IOException; +import java.net.URI; +import java.util.Map; + +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.opensearch.common.Strings; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; + +/** + * This class holds the content of an CustomWebhook message + */ +public class LegacyCustomWebhookMessage extends LegacyBaseMessage { + + private final String message; + private final String url; + private final String scheme; + private final String host; + private final String method; + private final int port; + private String path; + private final Map queryParams; + private Map headerParams; + + private LegacyCustomWebhookMessage( + final String destinationName, + final String url, + final String scheme, + final String host, + final Integer port, + final String path, + final String method, + final Map queryParams, + final Map headerParams, + final String message + ) { + super(LegacyDestinationType.LEGACY_CUSTOM_WEBHOOK, destinationName, message); + + if (!Strings.isNullOrEmpty(url)) { + setUrl(url.trim()); + } + + if (Strings.isNullOrEmpty(message)) { + throw new IllegalArgumentException("Message content is missing"); + } + + this.scheme = Strings.isNullOrEmpty(scheme) ? "https" : scheme; + this.port = port == null ? -1 : port; + + if (!Strings.isNullOrEmpty(path)) { + if (!path.startsWith("/")) { + this.path = "/" + path; + } + } + + if (Strings.isNullOrEmpty(url) && Strings.isNullOrEmpty(host)) { + throw new IllegalArgumentException("Either fully qualified URL or host name should be provided"); + } + + if (Strings.isNullOrEmpty(method)) { + // Default to POST for backwards compatibility + this.method = "POST"; + } else if (!HttpPost.METHOD_NAME.equals(method) && !HttpPut.METHOD_NAME.equals(method) && !HttpPatch.METHOD_NAME.equals(method)) { + throw new IllegalArgumentException("Invalid method supplied. Only POST, PUT and PATCH are allowed"); + } else { + this.method = method; + } + + this.message = message; + this.url = url; + this.host = host; + this.queryParams = queryParams; + this.headerParams = headerParams; + } + + public LegacyCustomWebhookMessage(StreamInput streamInput) throws IOException { + super(streamInput); + this.message = super.getMessageContent(); + this.url = streamInput.readOptionalString(); + this.scheme = null; + this.host = null; + this.method = streamInput.readOptionalString(); + this.port = -1; + this.path = null; + this.queryParams = null; + if (streamInput.readBoolean()) { + @SuppressWarnings("unchecked") + Map headerParams = (Map) (Map) streamInput.readMap(); + this.headerParams = headerParams; + } + } + + @Override + public String toString() { + return "DestinationType: " + + getChannelType() + + ", DestinationName:" + + destinationName + + ", Url: " + + url + + ", scheme: " + + scheme + + ", Host: " + + host + + ", Port: " + + port + + ", Path: " + + path + + ", Method: " + + method + + ", Message: <...>"; + } + + public static class Builder { + private String message; + private final String destinationName; + private String url; + private String scheme; + private String host; + private Integer port; + private String path; + private String method; + private Map queryParams; + private Map headerParams; + + public Builder(String destinationName) { + this.destinationName = destinationName; + } + + public LegacyCustomWebhookMessage.Builder withScheme(String scheme) { + this.scheme = scheme; + return this; + } + + public LegacyCustomWebhookMessage.Builder withHost(String host) { + this.host = host; + return this; + } + + public LegacyCustomWebhookMessage.Builder withPort(Integer port) { + this.port = port; + return this; + } + + public LegacyCustomWebhookMessage.Builder withPath(String path) { + this.path = path; + return this; + } + + public LegacyCustomWebhookMessage.Builder withMethod(String method) { + this.method = method; + return this; + } + + public LegacyCustomWebhookMessage.Builder withQueryParams(Map queryParams) { + this.queryParams = queryParams; + return this; + } + + public LegacyCustomWebhookMessage.Builder withHeaderParams(Map headerParams) { + this.headerParams = headerParams; + return this; + } + + public LegacyCustomWebhookMessage.Builder withMessage(String message) { + this.message = message; + return this; + } + + public LegacyCustomWebhookMessage.Builder withUrl(String url) { + this.url = url; + return this; + } + + public LegacyCustomWebhookMessage build() { + return new LegacyCustomWebhookMessage( + this.destinationName, + this.url, + this.scheme, + this.host, + this.port, + this.path, + this.method, + this.queryParams, + this.headerParams, + this.message + ); + } + } + + public String getScheme() { + return scheme; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + public String getPath() { + return path; + } + + public String getMethod() { + return method; + } + + public Map getQueryParams() { + return queryParams; + } + + public Map getHeaderParams() { + return headerParams; + } + + public URI getUri() { + return buildUri(getUrl(), getScheme(), getHost(), getPort(), getPath(), getQueryParams()); + } + + public String getMessage() { + return message; + } + + @Override + public void writeTo(StreamOutput streamOutput) throws IOException { + super.writeTo(streamOutput); + // Making LegacyCustomWebhookMessage streamable is purely to support the new pass through API from ISM -> Notification plugin + // and it only supports LegacyCustomWebhookMessage when the url is already constructed by ISM. + if (Strings.isNullOrEmpty(getUrl())) { + throw new IllegalStateException("Cannot use LegacyCustomWebhookMessage across transport wire without defining full url."); + } + streamOutput.writeOptionalString(url); + streamOutput.writeOptionalString(method); + streamOutput.writeBoolean(headerParams != null); + if (headerParams != null) { + @SuppressWarnings("unchecked") + Map headerParams = (Map) (Map) this.headerParams; + streamOutput.writeMap(headerParams); + } + } +} diff --git a/src/main/java/org/opensearch/commons/destination/message/LegacyDestinationType.java b/src/main/java/org/opensearch/commons/destination/message/LegacyDestinationType.java new file mode 100644 index 00000000..85689510 --- /dev/null +++ b/src/main/java/org/opensearch/commons/destination/message/LegacyDestinationType.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.opensearch.commons.destination.message; + +/** + * Supported legacy notification destinations for Index Management + */ +public enum LegacyDestinationType { + LEGACY_CHIME, + LEGACY_SLACK, + LEGACY_CUSTOM_WEBHOOK +} diff --git a/src/main/java/org/opensearch/commons/destination/message/LegacySlackMessage.java b/src/main/java/org/opensearch/commons/destination/message/LegacySlackMessage.java new file mode 100644 index 00000000..f426894a --- /dev/null +++ b/src/main/java/org/opensearch/commons/destination/message/LegacySlackMessage.java @@ -0,0 +1,95 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.opensearch.commons.destination.message; + +import java.io.IOException; + +import org.opensearch.common.Strings; +import org.opensearch.common.io.stream.StreamInput; + +/** + * This class holds the content of an Slack message + */ +public class LegacySlackMessage extends LegacyBaseMessage { + private final String message; + + private LegacySlackMessage(final String destinationName, final String url, final String message) { + super(LegacyDestinationType.LEGACY_SLACK, destinationName, message, url); + + if (Strings.isNullOrEmpty(url)) { // add URL validation + throw new IllegalArgumentException("Fully qualified URL is missing/invalid: " + url); + } + + if (Strings.isNullOrEmpty(message)) { + throw new IllegalArgumentException("Message content is missing"); + } + + this.message = message; + } + + public LegacySlackMessage(StreamInput streamInput) throws IOException { + super(streamInput); + this.message = super.getMessageContent(); + } + + @Override + public String toString() { + return "DestinationType: " + getChannelType() + ", DestinationName:" + destinationName + ", Url: " + url + ", Message: <...>"; + } + + public static class Builder { + private String message; + private String destinationName; + private String url; + + public Builder(String channelName) { + this.destinationName = channelName; + } + + public LegacySlackMessage.Builder withMessage(String message) { + this.message = message; + return this; + } + + public LegacySlackMessage.Builder withUrl(String url) { + this.url = url; + return this; + } + + public LegacySlackMessage build() { + return new LegacySlackMessage(this.destinationName, this.url, this.message); + } + } + + public String getMessage() { + return message; + } + + public String getUrl() { + return url; + } +} diff --git a/src/main/java/org/opensearch/commons/destination/response/LegacyBaseResponse.java b/src/main/java/org/opensearch/commons/destination/response/LegacyBaseResponse.java new file mode 100644 index 00000000..bbdd8d70 --- /dev/null +++ b/src/main/java/org/opensearch/commons/destination/response/LegacyBaseResponse.java @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.opensearch.commons.destination.response; + +import java.io.IOException; + +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; +import org.opensearch.common.io.stream.Writeable; + +/** + * This class holds the generic response attributes + */ +public abstract class LegacyBaseResponse implements Writeable { + protected Integer statusCode; + + LegacyBaseResponse(final Integer statusCode) { + if (statusCode == null) { + throw new IllegalArgumentException("status code is invalid"); + } + this.statusCode = statusCode; + } + + public LegacyBaseResponse(StreamInput streamInput) throws IOException { + this.statusCode = streamInput.readInt(); + } + + public int getStatusCode() { + return statusCode; + } + + @Override + public void writeTo(StreamOutput streamOutput) throws IOException { + streamOutput.writeInt(statusCode); + } +} diff --git a/src/main/java/org/opensearch/commons/destination/response/LegacyDestinationResponse.java b/src/main/java/org/opensearch/commons/destination/response/LegacyDestinationResponse.java new file mode 100644 index 00000000..bb927747 --- /dev/null +++ b/src/main/java/org/opensearch/commons/destination/response/LegacyDestinationResponse.java @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.opensearch.commons.destination.response; + +import java.io.IOException; + +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.StreamOutput; + +/** + * This class is a place holder for destination response metadata + */ +public class LegacyDestinationResponse extends LegacyBaseResponse { + + private final String responseContent; + + private LegacyDestinationResponse(final String responseString, final Integer statusCode) { + super(statusCode); + if (responseString == null) { + throw new IllegalArgumentException("Response is missing"); + } + this.responseContent = responseString; + } + + public LegacyDestinationResponse(StreamInput streamInput) throws IOException { + super(streamInput); + this.responseContent = streamInput.readString(); + } + + public static class Builder { + private String responseContent; + private Integer statusCode; + + public LegacyDestinationResponse.Builder withResponseContent(String responseContent) { + this.responseContent = responseContent; + return this; + } + + public LegacyDestinationResponse.Builder withStatusCode(Integer statusCode) { + this.statusCode = statusCode; + return this; + } + + public LegacyDestinationResponse build() { + return new LegacyDestinationResponse(responseContent, statusCode); + } + } + + public String getResponseContent() { + return this.responseContent; + } + + @Override + public void writeTo(StreamOutput streamOutput) throws IOException { + super.writeTo(streamOutput); + streamOutput.writeString(responseContent); + } +} diff --git a/src/main/kotlin/org/opensearch/commons/notifications/NotificationsPluginInterface.kt b/src/main/kotlin/org/opensearch/commons/notifications/NotificationsPluginInterface.kt index 2f043351..c0646d2d 100644 --- a/src/main/kotlin/org/opensearch/commons/notifications/NotificationsPluginInterface.kt +++ b/src/main/kotlin/org/opensearch/commons/notifications/NotificationsPluginInterface.kt @@ -31,6 +31,7 @@ import org.opensearch.action.ActionResponse import org.opensearch.client.node.NodeClient import org.opensearch.common.io.stream.Writeable import org.opensearch.commons.ConfigConstants.OPENSEARCH_SECURITY_USER_INFO_THREAD_CONTEXT +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_INDEX_MANAGEMENT import org.opensearch.commons.notifications.action.BaseResponse import org.opensearch.commons.notifications.action.CreateNotificationConfigRequest import org.opensearch.commons.notifications.action.CreateNotificationConfigResponse @@ -44,12 +45,15 @@ import org.opensearch.commons.notifications.action.GetNotificationEventRequest import org.opensearch.commons.notifications.action.GetNotificationEventResponse import org.opensearch.commons.notifications.action.GetPluginFeaturesRequest import org.opensearch.commons.notifications.action.GetPluginFeaturesResponse +import org.opensearch.commons.notifications.action.LegacyPublishNotificationRequest +import org.opensearch.commons.notifications.action.LegacyPublishNotificationResponse import org.opensearch.commons.notifications.action.NotificationsActions.CREATE_NOTIFICATION_CONFIG_ACTION_TYPE import org.opensearch.commons.notifications.action.NotificationsActions.DELETE_NOTIFICATION_CONFIG_ACTION_TYPE import org.opensearch.commons.notifications.action.NotificationsActions.GET_FEATURE_CHANNEL_LIST_ACTION_TYPE import org.opensearch.commons.notifications.action.NotificationsActions.GET_NOTIFICATION_CONFIG_ACTION_TYPE import org.opensearch.commons.notifications.action.NotificationsActions.GET_NOTIFICATION_EVENT_ACTION_TYPE import org.opensearch.commons.notifications.action.NotificationsActions.GET_PLUGIN_FEATURES_ACTION_TYPE +import org.opensearch.commons.notifications.action.NotificationsActions.LEGACY_PUBLISH_NOTIFICATION_ACTION_TYPE import org.opensearch.commons.notifications.action.NotificationsActions.SEND_NOTIFICATION_ACTION_TYPE import org.opensearch.commons.notifications.action.NotificationsActions.UPDATE_NOTIFICATION_CONFIG_ACTION_TYPE import org.opensearch.commons.notifications.action.SendNotificationRequest @@ -217,6 +221,30 @@ object NotificationsPluginInterface { ) } + /** + * Publishes a notification API using the legacy notification implementation. No REST API. + * Internal API only for the Index Management plugin. + * @param client Node client for making transport action + * @param request The legacy publish notification request + * @param listener The listener for getting response + */ + fun publishLegacyNotification( + client: NodeClient, + request: LegacyPublishNotificationRequest, + listener: ActionListener + ) { + if (request.feature != FEATURE_INDEX_MANAGEMENT) { + // Do not change this; do not pass in FEATURE_INDEX_MANAGEMENT if you are not the Index Management plugin. + throw IllegalArgumentException("The publish notification method only supports the Index Management feature.") + } + + client.execute( + LEGACY_PUBLISH_NOTIFICATION_ACTION_TYPE, + request, + wrapActionListener(listener) { response -> recreateObject(response) { LegacyPublishNotificationResponse(it) } } + ) + } + /** * Wrap action listener on concrete response class by a new created one on ActionResponse. * This is required because the response may be loaded by different classloader across plugins. diff --git a/src/main/kotlin/org/opensearch/commons/notifications/action/LegacyPublishNotificationRequest.kt b/src/main/kotlin/org/opensearch/commons/notifications/action/LegacyPublishNotificationRequest.kt new file mode 100644 index 00000000..e5807102 --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/notifications/action/LegacyPublishNotificationRequest.kt @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.commons.notifications.action + +import org.opensearch.action.ActionRequest +import org.opensearch.action.ActionRequestValidationException +import org.opensearch.common.io.stream.StreamInput +import org.opensearch.common.io.stream.StreamOutput +import org.opensearch.common.io.stream.Writeable +import org.opensearch.commons.destination.message.LegacyBaseMessage +import org.opensearch.commons.destination.message.LegacyChimeMessage +import org.opensearch.commons.destination.message.LegacyCustomWebhookMessage +import org.opensearch.commons.destination.message.LegacyDestinationType +import org.opensearch.commons.destination.message.LegacySlackMessage +import java.io.IOException + +/** + * Action Request to publish notification. This is a legacy implementation. + * This should not be used going forward, instead use [SendNotificationRequest]. + */ +class LegacyPublishNotificationRequest : ActionRequest { + val baseMessage: LegacyBaseMessage + val feature: String + + companion object { + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { LegacyPublishNotificationRequest(it) } + } + + /** + * constructor for creating the class + * @param baseMessage the base message to send + * @param feature the feature that is trying to use this request + */ + constructor( + baseMessage: LegacyBaseMessage, + feature: String + ) { + this.baseMessage = baseMessage + this.feature = feature + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + constructor(input: StreamInput) : super(input) { + baseMessage = when (requireNotNull(input.readEnum(LegacyDestinationType::class.java)) { "Destination type cannot be null" }) { + LegacyDestinationType.LEGACY_CHIME -> LegacyChimeMessage(input) + LegacyDestinationType.LEGACY_CUSTOM_WEBHOOK -> LegacyCustomWebhookMessage(input) + LegacyDestinationType.LEGACY_SLACK -> LegacySlackMessage(input) + } + feature = input.readString() + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + override fun writeTo(output: StreamOutput) { + super.writeTo(output) + output.writeEnum(baseMessage.channelType) + baseMessage.writeTo(output) + output.writeString(feature) + } + + /** + * {@inheritDoc} + */ + override fun validate(): ActionRequestValidationException? = null +} diff --git a/src/main/kotlin/org/opensearch/commons/notifications/action/LegacyPublishNotificationResponse.kt b/src/main/kotlin/org/opensearch/commons/notifications/action/LegacyPublishNotificationResponse.kt new file mode 100644 index 00000000..60f68e71 --- /dev/null +++ b/src/main/kotlin/org/opensearch/commons/notifications/action/LegacyPublishNotificationResponse.kt @@ -0,0 +1,63 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.commons.notifications.action + +import org.opensearch.common.io.stream.StreamInput +import org.opensearch.common.io.stream.StreamOutput +import org.opensearch.common.io.stream.Writeable +import org.opensearch.common.xcontent.ToXContent +import org.opensearch.common.xcontent.XContentBuilder +import org.opensearch.commons.destination.response.LegacyDestinationResponse +import java.io.IOException + +/** + * Action Response for legacy publish notification. + */ +class LegacyPublishNotificationResponse : BaseResponse { + val destinationResponse: LegacyDestinationResponse + + companion object { + /** + * reader to create instance of class from writable. + */ + val reader = Writeable.Reader { LegacyPublishNotificationResponse(it) } + } + + /** + * constructor for creating the class + * @param destinationResponse the response of the published notification + */ + constructor(destinationResponse: LegacyDestinationResponse) { + this.destinationResponse = destinationResponse + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + constructor(input: StreamInput) : super(input) { + destinationResponse = LegacyDestinationResponse(input) + } + + /** + * {@inheritDoc} + */ + @Throws(IOException::class) + override fun writeTo(output: StreamOutput) { + destinationResponse.writeTo(output) + } + + // This class is only used across transport wire and does not need to implement toXContent + override fun toXContent(builder: XContentBuilder, params: ToXContent.Params): XContentBuilder { + throw IllegalStateException("Legacy notification response is not intended for REST or persistence and does not support XContent.") + } +} diff --git a/src/main/kotlin/org/opensearch/commons/notifications/action/NotificationsActions.kt b/src/main/kotlin/org/opensearch/commons/notifications/action/NotificationsActions.kt index fe7457d0..d93dd29a 100644 --- a/src/main/kotlin/org/opensearch/commons/notifications/action/NotificationsActions.kt +++ b/src/main/kotlin/org/opensearch/commons/notifications/action/NotificationsActions.kt @@ -72,6 +72,12 @@ object NotificationsActions { */ const val SEND_NOTIFICATION_NAME = "cluster:admin/opensearch/notifications/feature/send" + /** + * Publish legacy notification message. Internal only - Inter plugin communication. + * Only for the Index Management plugin. + */ + const val LEGACY_PUBLISH_NOTIFICATION_NAME = "cluster:admin/opensearch/notifications/feature/publish" + /** * Create notification configuration transport action type. */ @@ -119,4 +125,11 @@ object NotificationsActions { */ val SEND_NOTIFICATION_ACTION_TYPE = ActionType(SEND_NOTIFICATION_NAME, ::SendNotificationResponse) + + /** + * Send legacy notification transport action type. Internal only - Inter plugin communication. + * Only for the Index Management plugin. + */ + val LEGACY_PUBLISH_NOTIFICATION_ACTION_TYPE = + ActionType(LEGACY_PUBLISH_NOTIFICATION_NAME, ::LegacyPublishNotificationResponse) } diff --git a/src/test/java/org/opensearch/commons/destination/message/LegacyChimeMessageTest.java b/src/test/java/org/opensearch/commons/destination/message/LegacyChimeMessageTest.java new file mode 100644 index 00000000..4477789e --- /dev/null +++ b/src/test/java/org/opensearch/commons/destination/message/LegacyChimeMessageTest.java @@ -0,0 +1,100 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.opensearch.commons.destination.message; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.common.io.stream.StreamInput; + +public class LegacyChimeMessageTest { + + @Test + public void testBuildingLegacyChimeMessage() { + LegacyChimeMessage message = new LegacyChimeMessage.Builder("custom_webhook") + .withMessage("Hello world") + .withUrl("https://amazon.com") + .build(); + + assertEquals("custom_webhook", message.destinationName); + assertEquals(LegacyDestinationType.LEGACY_CHIME, message.getChannelType()); + assertEquals("Hello world", message.getMessageContent()); + assertEquals("https://amazon.com", message.url); + } + + @Test + public void testRoundTrippingLegacyChimeMessage() throws IOException { + LegacyChimeMessage message = new LegacyChimeMessage.Builder("custom_webhook") + .withMessage("Hello world") + .withUrl("https://amazon.com") + .build(); + BytesStreamOutput out = new BytesStreamOutput(); + message.writeTo(out); + + StreamInput in = StreamInput.wrap(out.bytes().toBytesRef().bytes); + LegacyChimeMessage newMessage = new LegacyChimeMessage(in); + + assertEquals(newMessage.destinationName, message.destinationName); + assertEquals(newMessage.getChannelType(), message.getChannelType()); + assertEquals(newMessage.getMessageContent(), message.getMessageContent()); + assertEquals(newMessage.url, message.url); + } + + @Test + public void testContentMissingMessage() { + try { + new LegacyChimeMessage.Builder("custom_webhook").withUrl("https://amazon.com").build(); + fail("Building legacy chime message without message should fail"); + } catch (IllegalArgumentException e) { + assertEquals("Message content is missing", e.getMessage()); + } + } + + @Test + public void testUrlMissingMessage() { + try { + new LegacyChimeMessage.Builder("custom_webhook").withMessage("Hello world").build(); + fail("Building legacy chime message without url should fail"); + } catch (IllegalArgumentException e) { + assertEquals("url is invalid or empty", e.getMessage()); + } + } + + @Test + public void testMissingDestinationName() { + try { + new LegacyChimeMessage.Builder(null).withMessage("Hello world").withUrl("https://amazon.com").build(); + fail("Building legacy chime message with null destination name should fail"); + } catch (IllegalArgumentException e) { + assertEquals("Channel name must be defined", e.getMessage()); + } + } +} diff --git a/src/test/java/org/opensearch/commons/destination/message/LegacyCustomWebhookMessageTest.java b/src/test/java/org/opensearch/commons/destination/message/LegacyCustomWebhookMessageTest.java new file mode 100644 index 00000000..fba614fd --- /dev/null +++ b/src/test/java/org/opensearch/commons/destination/message/LegacyCustomWebhookMessageTest.java @@ -0,0 +1,177 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.opensearch.commons.destination.message; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.junit.jupiter.api.Test; +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.common.io.stream.StreamInput; + +public class LegacyCustomWebhookMessageTest { + + @Test + public void testBuildingLegacyCustomWebhookMessage() { + LegacyCustomWebhookMessage message = new LegacyCustomWebhookMessage.Builder("custom_webhook") + .withMessage("Hello world") + .withUrl("https://amazon.com") + .build(); + + assertEquals("custom_webhook", message.destinationName); + assertEquals(LegacyDestinationType.LEGACY_CUSTOM_WEBHOOK, message.getChannelType()); + assertEquals("Hello world", message.getMessageContent()); + assertEquals("https://amazon.com", message.getUrl()); + } + + @Test + public void testRoundTrippingLegacyCustomWebhookMessageWithUrl() throws IOException { + LegacyCustomWebhookMessage message = new LegacyCustomWebhookMessage.Builder("custom_webhook") + .withMessage("Hello world") + .withUrl("https://amazon.com") + .build(); + BytesStreamOutput out = new BytesStreamOutput(); + message.writeTo(out); + + StreamInput in = StreamInput.wrap(out.bytes().toBytesRef().bytes); + LegacyCustomWebhookMessage newMessage = new LegacyCustomWebhookMessage(in); + + assertEquals(newMessage.destinationName, message.destinationName); + assertEquals(newMessage.getChannelType(), message.getChannelType()); + assertEquals(newMessage.getMessageContent(), message.getMessageContent()); + assertEquals(newMessage.getUrl(), message.getUrl()); + } + + @Test + public void testRoundTrippingLegacyCustomWebhookMessageWithHostFails() throws IOException { + Map queryParams = new HashMap(); + queryParams.put("token", "sometoken"); + Map headers = new HashMap(); + headers.put("x-token", "sometoken"); + LegacyCustomWebhookMessage message = new LegacyCustomWebhookMessage.Builder("custom_webhook") + .withMessage("Hello world") + .withHost("hooks.chime.aws") + .withPath("incomingwebhooks/abc") + .withMethod(HttpPost.METHOD_NAME) + .withQueryParams(queryParams) + .withHeaderParams(headers) + .withPort(8000) + .withScheme("https") + .build(); + BytesStreamOutput out = new BytesStreamOutput(); + try { + message.writeTo(out); + fail("Writing LegacyCustomWebhookMessage with host instead of url to stream output should fail"); + } catch (IllegalStateException e) { + assertEquals("Cannot use LegacyCustomWebhookMessage across transport wire without defining full url.", e.getMessage()); + } + } + + @Test + public void testContentMissingMessage() { + try { + new LegacyCustomWebhookMessage.Builder("custom_webhook").withUrl("https://amazon.com").build(); + fail("Building legacy custom webhook message without message should fail"); + } catch (IllegalArgumentException e) { + assertEquals("Message content is missing", e.getMessage()); + } + } + + @Test + public void testMissingDestinationName() { + try { + new LegacyCustomWebhookMessage.Builder(null).withMessage("Hello world").withUrl("https://amazon.com").build(); + fail("Building legacy custom webhook message with null destination name should fail"); + } catch (IllegalArgumentException e) { + assertEquals("Channel name must be defined", e.getMessage()); + } + } + + @Test + public void testUnsupportedHttpMethods() { + try { + new LegacyCustomWebhookMessage.Builder("custom_webhook") + .withMessage("Hello world") + .withUrl("https://amazon.com") + .withMethod(HttpGet.METHOD_NAME) + .build(); + fail("Building legacy custom webhook message with unsupported http methods should fail"); + } catch (IllegalArgumentException e) { + assertEquals("Invalid method supplied. Only POST, PUT and PATCH are allowed", e.getMessage()); + } + } + + @Test + public void testURLandHostNameMissingOrEmpty() { + try { + new LegacyCustomWebhookMessage.Builder("custom_webhook").withMessage("Hello world").withMethod(HttpGet.METHOD_NAME).build(); + fail("Building legacy custom webhook message missing or empty url and host name should fail"); + } catch (IllegalArgumentException e) { + assertEquals("Either fully qualified URL or host name should be provided", e.getMessage()); + } + + try { + new LegacyCustomWebhookMessage.Builder("custom_webhook") + .withMessage("Hello world") + .withUrl("") + .withMethod(HttpGet.METHOD_NAME) + .build(); + fail("Building legacy custom webhook message with missing or empty url and host name should fail"); + } catch (IllegalArgumentException e) { + assertEquals("Either fully qualified URL or host name should be provided", e.getMessage()); + } + + try { + new LegacyCustomWebhookMessage.Builder("custom_webhook") + .withMessage("Hello world") + .withHost("") + .withMethod(HttpGet.METHOD_NAME) + .build(); + fail("Building legacy custom webhook message with missing or empty url and host name should fail"); + } catch (IllegalArgumentException e) { + assertEquals("Either fully qualified URL or host name should be provided", e.getMessage()); + } + + try { + new LegacyCustomWebhookMessage.Builder("custom_webhook") + .withMessage("Hello world") + .withUrl("") + .withHost("") + .withMethod(HttpGet.METHOD_NAME) + .build(); + fail("Building legacy custom webhook message with missing or empty url and host name should fail"); + } catch (IllegalArgumentException e) { + assertEquals("Either fully qualified URL or host name should be provided", e.getMessage()); + } + } +} diff --git a/src/test/java/org/opensearch/commons/destination/message/LegacySlackMessageTest.java b/src/test/java/org/opensearch/commons/destination/message/LegacySlackMessageTest.java new file mode 100644 index 00000000..a520fc8e --- /dev/null +++ b/src/test/java/org/opensearch/commons/destination/message/LegacySlackMessageTest.java @@ -0,0 +1,110 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.opensearch.commons.destination.message; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.common.io.stream.StreamInput; + +public class LegacySlackMessageTest { + + @Test + public void testBuildingLegacySlackMessage() { + LegacySlackMessage message = new LegacySlackMessage.Builder("custom_webhook") + .withMessage("Hello world") + .withUrl("https://amazon.com") + .build(); + + assertEquals("custom_webhook", message.destinationName); + assertEquals(LegacyDestinationType.LEGACY_SLACK, message.getChannelType()); + assertEquals("Hello world", message.getMessageContent()); + assertEquals("https://amazon.com", message.url); + } + + @Test + public void testRoundTrippingLegacySlackMessage() throws IOException { + LegacySlackMessage message = new LegacySlackMessage.Builder("custom_webhook") + .withMessage("Hello world") + .withUrl("https://amazon.com") + .build(); + BytesStreamOutput out = new BytesStreamOutput(); + message.writeTo(out); + + StreamInput in = StreamInput.wrap(out.bytes().toBytesRef().bytes); + LegacySlackMessage newMessage = new LegacySlackMessage(in); + + assertEquals(newMessage.destinationName, message.destinationName); + assertEquals(newMessage.getChannelType(), message.getChannelType()); + assertEquals(newMessage.getMessageContent(), message.getMessageContent()); + assertEquals(newMessage.url, message.url); + } + + @Test + public void testContentMissingMessage() { + try { + new LegacySlackMessage.Builder("custom_webhook").withUrl("https://amazon.com").build(); + fail("Building legacy slack message without message should fail"); + } catch (IllegalArgumentException e) { + assertEquals("Message content is missing", e.getMessage()); + } + } + + @Test + public void testUrlMissingMessage() { + try { + new LegacySlackMessage.Builder("custom_webhook").withMessage("Hello world").build(); + fail("Building legacy slack message without url should fail"); + } catch (IllegalArgumentException e) { + assertEquals("url is invalid or empty", e.getMessage()); + } + } + + @Test + public void testMissingDestinationName() { + try { + new LegacySlackMessage.Builder(null).withMessage("Hello world").withUrl("https://amazon.com").build(); + fail("Building legacy slack message with null destination name should fail"); + } catch (IllegalArgumentException e) { + assertEquals("Channel name must be defined", e.getMessage()); + } + } + + @Test + public void testUrlEmptyMessage() { + try { + new LegacySlackMessage.Builder("custom_webhook").withMessage("Hello world").withUrl("").build(); + fail("Building legacy slack message with empty url should fail"); + } catch (IllegalArgumentException e) { + assertEquals("Fully qualified URL is missing/invalid: ", e.getMessage()); + } + } +} diff --git a/src/test/java/org/opensearch/commons/destination/response/LegacyDestinationResponseTest.java b/src/test/java/org/opensearch/commons/destination/response/LegacyDestinationResponseTest.java new file mode 100644 index 00000000..6175827e --- /dev/null +++ b/src/test/java/org/opensearch/commons/destination/response/LegacyDestinationResponseTest.java @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.opensearch.commons.destination.response; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.common.io.stream.StreamInput; + +public class LegacyDestinationResponseTest { + + @Test + public void testBuildingLegacyDestinationResponse() { + LegacyDestinationResponse res = new LegacyDestinationResponse.Builder() + .withStatusCode(200) + .withResponseContent("Hello world") + .build(); + + assertEquals(200, res.statusCode); + assertEquals("Hello world", res.getResponseContent()); + } + + @Test + public void testRoundTrippingLegacyDestinationResponse() throws IOException { + LegacyDestinationResponse res = new LegacyDestinationResponse.Builder() + .withStatusCode(200) + .withResponseContent("Hello world") + .build(); + BytesStreamOutput out = new BytesStreamOutput(); + res.writeTo(out); + + StreamInput in = StreamInput.wrap(out.bytes().toBytesRef().bytes); + LegacyDestinationResponse newRes = new LegacyDestinationResponse(in); + + assertEquals(res.statusCode, newRes.statusCode, "Round tripping doesn't work"); + assertEquals(res.getResponseContent(), newRes.getResponseContent(), "Round tripping doesn't work"); + } + + @Test + public void testMissingLegacyDestinationResponse() { + try { + new LegacyDestinationResponse.Builder().withStatusCode(200).build(); + fail("Creating LegacyDestinationResponse without response content should fail"); + } catch (IllegalArgumentException ignored) {} + } + + @Test + public void testMissingLegacyDestinationStatusCode() { + try { + new LegacyDestinationResponse.Builder().withResponseContent("Hello world").build(); + fail("Creating LegacyDestinationResponse without status code should fail"); + } catch (IllegalArgumentException ignored) {} + } +} diff --git a/src/test/kotlin/org/opensearch/commons/notifications/NotificationsPluginInterfaceTests.kt b/src/test/kotlin/org/opensearch/commons/notifications/NotificationsPluginInterfaceTests.kt index cf4f8420..343fecbd 100644 --- a/src/test/kotlin/org/opensearch/commons/notifications/NotificationsPluginInterfaceTests.kt +++ b/src/test/kotlin/org/opensearch/commons/notifications/NotificationsPluginInterfaceTests.kt @@ -25,7 +25,9 @@ import org.mockito.junit.jupiter.MockitoExtension import org.opensearch.action.ActionListener import org.opensearch.action.ActionType import org.opensearch.client.node.NodeClient +import org.opensearch.commons.destination.response.LegacyDestinationResponse import org.opensearch.commons.notifications.NotificationConstants.FEATURE_ALERTING +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_INDEX_MANAGEMENT import org.opensearch.commons.notifications.NotificationConstants.FEATURE_REPORTS import org.opensearch.commons.notifications.action.CreateNotificationConfigRequest import org.opensearch.commons.notifications.action.CreateNotificationConfigResponse @@ -39,6 +41,8 @@ import org.opensearch.commons.notifications.action.GetNotificationEventRequest import org.opensearch.commons.notifications.action.GetNotificationEventResponse import org.opensearch.commons.notifications.action.GetPluginFeaturesRequest import org.opensearch.commons.notifications.action.GetPluginFeaturesResponse +import org.opensearch.commons.notifications.action.LegacyPublishNotificationRequest +import org.opensearch.commons.notifications.action.LegacyPublishNotificationResponse import org.opensearch.commons.notifications.action.SendNotificationResponse import org.opensearch.commons.notifications.action.UpdateNotificationConfigRequest import org.opensearch.commons.notifications.action.UpdateNotificationConfigResponse @@ -223,6 +227,26 @@ internal class NotificationsPluginInterfaceTests { verify(listener, times(1)).onResponse(eq(response)) } + @Test + fun publishLegacyNotification() { + val request = mock(LegacyPublishNotificationRequest::class.java) + val res = LegacyPublishNotificationResponse(LegacyDestinationResponse.Builder().withStatusCode(200).withResponseContent("Nice!").build()) + val l: ActionListener = + mock(ActionListener::class.java) as ActionListener + + doAnswer { + (it.getArgument(2) as ActionListener) + .onResponse(res) + }.whenever(client).execute(any(ActionType::class.java), any(), any()) + + doAnswer { + FEATURE_INDEX_MANAGEMENT + }.whenever(request).feature + + NotificationsPluginInterface.publishLegacyNotification(client, request, l) + verify(l, times(1)).onResponse(eq(res)) + } + private fun mockGetNotificationConfigResponse(): GetNotificationConfigResponse { val sampleSlack = Slack("https://domain.com/sample_url#1234567890") val sampleConfig = NotificationConfig( diff --git a/src/test/kotlin/org/opensearch/commons/notifications/action/LegacyPublishNotificationRequestTests.kt b/src/test/kotlin/org/opensearch/commons/notifications/action/LegacyPublishNotificationRequestTests.kt new file mode 100644 index 00000000..349a1c14 --- /dev/null +++ b/src/test/kotlin/org/opensearch/commons/notifications/action/LegacyPublishNotificationRequestTests.kt @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.commons.notifications.action + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Test +import org.opensearch.commons.destination.message.LegacyChimeMessage +import org.opensearch.commons.notifications.NotificationConstants.FEATURE_INDEX_MANAGEMENT +import org.opensearch.commons.utils.recreateObject + +internal class LegacyPublishNotificationRequestTests { + + private fun assertRequestEquals( + expected: LegacyPublishNotificationRequest, + actual: LegacyPublishNotificationRequest + ) { + assertEquals(expected.baseMessage.channelName, actual.baseMessage.channelName) + assertEquals(expected.baseMessage.channelType, actual.baseMessage.channelType) + assertEquals(expected.baseMessage.messageContent, actual.baseMessage.messageContent) + assertEquals(expected.baseMessage.url, actual.baseMessage.url) + assertEquals(expected.feature, actual.feature) + assertNull(actual.validate()) + } + + @Test + fun `publish request serialize and deserialize transport object should be equal`() { + val baseMessage = LegacyChimeMessage.Builder("chime_message").withMessage("Hello world").withUrl("https://amazon.com").build() + val request = LegacyPublishNotificationRequest(baseMessage, FEATURE_INDEX_MANAGEMENT) + val recreatedObject = recreateObject(request) { LegacyPublishNotificationRequest(it) } + assertRequestEquals(request, recreatedObject) + } +} diff --git a/src/test/kotlin/org/opensearch/commons/notifications/action/LegacyPublishNotificationResponseTests.kt b/src/test/kotlin/org/opensearch/commons/notifications/action/LegacyPublishNotificationResponseTests.kt new file mode 100644 index 00000000..4b6f8e0b --- /dev/null +++ b/src/test/kotlin/org/opensearch/commons/notifications/action/LegacyPublishNotificationResponseTests.kt @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.commons.notifications.action + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.opensearch.commons.destination.response.LegacyDestinationResponse +import org.opensearch.commons.utils.recreateObject + +internal class LegacyPublishNotificationResponseTests { + + @Test + fun `Create response serialize and deserialize transport object should be equal`() { + val res = LegacyDestinationResponse.Builder().withStatusCode(200).withResponseContent("Hello world").build() + val configResponse = LegacyPublishNotificationResponse(res) + val recreatedObject = recreateObject(configResponse) { LegacyPublishNotificationResponse(it) } + assertEquals(configResponse.destinationResponse.statusCode, recreatedObject.destinationResponse.statusCode) + assertEquals(configResponse.destinationResponse.responseContent, recreatedObject.destinationResponse.responseContent) + } +}