From 8c8a6b6a1124cc9485fe6d4124295a5e6fecd386 Mon Sep 17 00:00:00 2001 From: zhichao-aws Date: Thu, 24 Aug 2023 18:23:00 +0800 Subject: [PATCH] Microsoft teams (#676) * Added feature support for microsoft teams webhoo Signed-off-by: danielkyalo599 * Added feature support for microsoft teams webhook ,removed valid webhooks Signed-off-by: danielkyalo599 * Added feature support for Microsoft teams webhook Signed-off-by: danielkyalo599 * Refactored feature support for ms teams and added unit and integTest Signed-off-by: danielkyalo599 * fix build in core Signed-off-by: zhichao-aws * fix core-spi build Signed-off-by: zhichao-aws * fix notifications main code Signed-off-by: zhichao-aws * fix mappings, add IT Signed-off-by: zhichao-aws * add auto upgrade mapping logic Signed-off-by: zhichao-aws * put load mapping to initialize step Signed-off-by: zhichao-aws * add schema_version field Signed-off-by: zhichao-aws * add integ test Signed-off-by: zhichao-aws * adjust with auto upgrade mapping logic Signed-off-by: zhichao-aws * add bwc Signed-off-by: zhichao-aws * modify bwc Signed-off-by: zhichao-aws * modify bwc Signed-off-by: zhichao-aws * resolve comments Signed-off-by: zhichao-aws * add license header Signed-off-by: zhichao-aws * fix microsoft teams sample url in IT to adapt url validation Signed-off-by: zhichao-aws --------- Signed-off-by: danielkyalo599 Signed-off-by: zhichao-aws Co-authored-by: danielkyalo599 --- .../spi/model/destination/DestinationType.kt | 2 +- .../destination/MicrosoftTeamsDestination.kt | 13 ++ .../spi/utils/ValidationHelpersTests.kt | 5 + .../src/main/config/notifications-core.yml | 2 +- .../core/NotificationCoreImpl.kt | 2 +- .../core/client/DestinationHttpClient.kt | 5 +- .../core/setting/PluginSettings.kt | 1 + .../transport/DestinationTransportProvider.kt | 1 + .../core/NotificationCoreImplTests.kt | 1 + .../MicrosoftTeamsDestinationTests.kt | 182 ++++++++++++++++++ .../core/settings/PluginSettingsTests.kt | 1 + .../index/ConfigIndexingActions.kt | 9 +- .../notifications/index/ConfigQueryHelper.kt | 3 + .../notifications/metrics/Metrics.kt | 4 + .../NotificationConfigRestHandler.kt | 1 + .../send/SendMessageActionHelper.kt | 19 +- .../notifications-config-mapping.yml | 12 +- .../opensearch/integtest/IntegTestHelpers.kt | 3 + .../integtest/SecurityNotificationIT.kt | 3 +- .../NotificationsBackwardsCompatibilityIT.kt | 3 + .../config/CreateNotificationConfigIT.kt | 53 ++++- .../MicrosoftTeamsNotificationConfigCrudIT.kt | 158 +++++++++++++++ .../config/QueryNotificationConfigIT.kt | 35 +++- .../config/SlackNotificationConfigCrudIT.kt | 2 +- .../features/GetNotificationChannelListIT.kt | 3 +- .../send/SendTestMessageRestHandlerIT.kt | 33 +++- .../notifications/ObjectEqualsHelpers.kt | 6 + .../index/ConfigIndexingActionsTests.kt | 45 +++++ 28 files changed, 575 insertions(+), 32 deletions(-) create mode 100644 notifications/core-spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/MicrosoftTeamsDestination.kt create mode 100644 notifications/core/src/test/kotlin/org/opensearch/notifications/core/destinations/MicrosoftTeamsDestinationTests.kt create mode 100644 notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/MicrosoftTeamsNotificationConfigCrudIT.kt create mode 100644 notifications/notifications/src/test/kotlin/org/opensearch/notifications/index/ConfigIndexingActionsTests.kt diff --git a/notifications/core-spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/DestinationType.kt b/notifications/core-spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/DestinationType.kt index f915e174..9ed9a8ca 100644 --- a/notifications/core-spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/DestinationType.kt +++ b/notifications/core-spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/DestinationType.kt @@ -8,5 +8,5 @@ package org.opensearch.notifications.spi.model.destination * Supported notification destinations */ enum class DestinationType { - CHIME, SLACK, CUSTOM_WEBHOOK, SMTP, SES, SNS + CHIME, SLACK, MICROSOFT_TEAMS, CUSTOM_WEBHOOK, SMTP, SES, SNS } diff --git a/notifications/core-spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/MicrosoftTeamsDestination.kt b/notifications/core-spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/MicrosoftTeamsDestination.kt new file mode 100644 index 00000000..7200795f --- /dev/null +++ b/notifications/core-spi/src/main/kotlin/org/opensearch/notifications/spi/model/destination/MicrosoftTeamsDestination.kt @@ -0,0 +1,13 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.notifications.spi.model.destination + +/** + * This class holds the contents of a Microsoft Teams destination + */ +class MicrosoftTeamsDestination( + url: String, +) : WebhookDestination(url, DestinationType.MICROSOFT_TEAMS) diff --git a/notifications/core-spi/src/test/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpersTests.kt b/notifications/core-spi/src/test/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpersTests.kt index 0d803730..57f4e129 100644 --- a/notifications/core-spi/src/test/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpersTests.kt +++ b/notifications/core-spi/src/test/kotlin/org/opensearch/notifications/spi/utils/ValidationHelpersTests.kt @@ -22,6 +22,7 @@ internal class ValidationHelpersTests { private val LOCAL_HOST_EXTENDED = "https://localhost:6060/service" private val WEBHOOK_URL = "https://test-webhook.com:1234/subdirectory?param1=value1¶m2=¶m3=value3" private val CHIME_URL = "https://domain.com/sample_chime_url#1234567890" + private val MICROSOFT_TEAMS_WEBHOOK_URL = "https://test.webhook.office.com/webhookb2/12345678/IncomingWebhook/87654321" private val hostDenyList = listOf( "127.0.0.0/8", @@ -99,4 +100,8 @@ internal class ValidationHelpersTests { fun `validator identifies chime url as valid`() { assert(isValidUrl(CHIME_URL)) } + @Test + fun `validator identifies microsoft teams url as valid`() { + assert(isValidUrl(MICROSOFT_TEAMS_WEBHOOK_URL)) + } } diff --git a/notifications/core/src/main/config/notifications-core.yml b/notifications/core/src/main/config/notifications-core.yml index 57b2d613..9ae7ed40 100644 --- a/notifications/core/src/main/config/notifications-core.yml +++ b/notifications/core/src/main/config/notifications-core.yml @@ -15,5 +15,5 @@ opensearch.notifications.core: connection_timeout: 5000 # in milliseconds socket_timeout: 50000 host_deny_list: [] - allowed_config_types: ["slack","chime","webhook","email","sns","ses_account","smtp_account","email_group"] + allowed_config_types: ["slack","chime","microsoft_teams","webhook","email","sns","ses_account","smtp_account","email_group"] tooltip_support: true diff --git a/notifications/core/src/main/kotlin/org/opensearch/notifications/core/NotificationCoreImpl.kt b/notifications/core/src/main/kotlin/org/opensearch/notifications/core/NotificationCoreImpl.kt index 615cee1e..3cb2771e 100644 --- a/notifications/core/src/main/kotlin/org/opensearch/notifications/core/NotificationCoreImpl.kt +++ b/notifications/core/src/main/kotlin/org/opensearch/notifications/core/NotificationCoreImpl.kt @@ -16,7 +16,7 @@ import java.security.PrivilegedAction /** * This is a client facing NotificationCoreImpl class to send the messages - * to the NotificationCoreImpl channels like chime, slack, webhooks, email etc + * to the NotificationCoreImpl channels like chime, slack, Microsoft Teams, webhooks, email etc */ object NotificationCoreImpl : NotificationCore { /** diff --git a/notifications/core/src/main/kotlin/org/opensearch/notifications/core/client/DestinationHttpClient.kt b/notifications/core/src/main/kotlin/org/opensearch/notifications/core/client/DestinationHttpClient.kt index e65fe95f..e186eefe 100644 --- a/notifications/core/src/main/kotlin/org/opensearch/notifications/core/client/DestinationHttpClient.kt +++ b/notifications/core/src/main/kotlin/org/opensearch/notifications/core/client/DestinationHttpClient.kt @@ -31,6 +31,7 @@ import org.opensearch.notifications.core.utils.validateUrlHost import org.opensearch.notifications.spi.model.MessageContent import org.opensearch.notifications.spi.model.destination.ChimeDestination import org.opensearch.notifications.spi.model.destination.CustomWebhookDestination +import org.opensearch.notifications.spi.model.destination.MicrosoftTeamsDestination import org.opensearch.notifications.spi.model.destination.SlackDestination import org.opensearch.notifications.spi.model.destination.WebhookDestination import java.io.IOException @@ -159,12 +160,14 @@ class DestinationHttpClient { val keyName = when (destination) { // Slack webhook request body has required "text" as key name https://api.slack.com/messaging/webhooks // Chime webhook request body has required "Content" as key name + // Microsoft Teams webhook request body has required "text" as key name https://learn.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/what-are-webhooks-and-connectors // Customer webhook allows input as json or plain text, so we just return the message as it is is SlackDestination -> "text" is ChimeDestination -> "Content" + is MicrosoftTeamsDestination -> "text" is CustomWebhookDestination -> return message.textDescription else -> throw IllegalArgumentException( - "Invalid destination type is provided, Only Slack, Chime and CustomWebhook are allowed" + "Invalid destination type is provided, Only Slack, Chime, Microsoft Teams and CustomWebhook are allowed" ) } diff --git a/notifications/core/src/main/kotlin/org/opensearch/notifications/core/setting/PluginSettings.kt b/notifications/core/src/main/kotlin/org/opensearch/notifications/core/setting/PluginSettings.kt index d965f7d4..75bbeb1b 100644 --- a/notifications/core/src/main/kotlin/org/opensearch/notifications/core/setting/PluginSettings.kt +++ b/notifications/core/src/main/kotlin/org/opensearch/notifications/core/setting/PluginSettings.kt @@ -157,6 +157,7 @@ internal object PluginSettings { private val DEFAULT_ALLOWED_CONFIG_TYPES = listOf( "slack", "chime", + "microsoft_teams", "webhook", "email", "sns", diff --git a/notifications/core/src/main/kotlin/org/opensearch/notifications/core/transport/DestinationTransportProvider.kt b/notifications/core/src/main/kotlin/org/opensearch/notifications/core/transport/DestinationTransportProvider.kt index ed682951..24f6a174 100644 --- a/notifications/core/src/main/kotlin/org/opensearch/notifications/core/transport/DestinationTransportProvider.kt +++ b/notifications/core/src/main/kotlin/org/opensearch/notifications/core/transport/DestinationTransportProvider.kt @@ -24,6 +24,7 @@ internal object DestinationTransportProvider { var destinationTransportMap = mapOf( DestinationType.SLACK to webhookDestinationTransport, DestinationType.CHIME to webhookDestinationTransport, + DestinationType.MICROSOFT_TEAMS to webhookDestinationTransport, DestinationType.CUSTOM_WEBHOOK to webhookDestinationTransport, DestinationType.SMTP to smtpDestinationTransport, DestinationType.SNS to snsDestinationTransport, diff --git a/notifications/core/src/test/kotlin/org/opensearch/notifications/core/NotificationCoreImplTests.kt b/notifications/core/src/test/kotlin/org/opensearch/notifications/core/NotificationCoreImplTests.kt index 872840ec..bd9da840 100644 --- a/notifications/core/src/test/kotlin/org/opensearch/notifications/core/NotificationCoreImplTests.kt +++ b/notifications/core/src/test/kotlin/org/opensearch/notifications/core/NotificationCoreImplTests.kt @@ -14,6 +14,7 @@ class NotificationCoreImplTests { private val defaultConfigTypes = listOf( "slack", "chime", + "microsoft_teams", "webhook", "email", "sns", diff --git a/notifications/core/src/test/kotlin/org/opensearch/notifications/core/destinations/MicrosoftTeamsDestinationTests.kt b/notifications/core/src/test/kotlin/org/opensearch/notifications/core/destinations/MicrosoftTeamsDestinationTests.kt new file mode 100644 index 00000000..1fffa8da --- /dev/null +++ b/notifications/core/src/test/kotlin/org/opensearch/notifications/core/destinations/MicrosoftTeamsDestinationTests.kt @@ -0,0 +1,182 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.notifications.core.destinations + +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import org.apache.hc.client5.http.classic.methods.HttpPost +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse +import org.apache.hc.core5.http.io.entity.StringEntity +import org.easymock.EasyMock +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.opensearch.core.rest.RestStatus +import org.opensearch.notifications.core.NotificationCoreImpl +import org.opensearch.notifications.core.client.DestinationHttpClient +import org.opensearch.notifications.core.transport.DestinationTransportProvider +import org.opensearch.notifications.core.transport.WebhookDestinationTransport +import org.opensearch.notifications.spi.model.DestinationMessageResponse +import org.opensearch.notifications.spi.model.MessageContent +import org.opensearch.notifications.spi.model.destination.ChimeDestination +import org.opensearch.notifications.spi.model.destination.DestinationType +import org.opensearch.notifications.spi.model.destination.MicrosoftTeamsDestination +import java.net.MalformedURLException +import java.util.stream.Stream + +internal class MicrosoftTeamsDestinationTests { + companion object { + @JvmStatic + fun escapeSequenceToRaw(): Stream = + Stream.of( + Arguments.of("\n", """\n"""), + Arguments.of("\t", """\t"""), + Arguments.of("\b", """\b"""), + Arguments.of("\r", """\r"""), + Arguments.of("\"", """\""""), + ) + } + + @BeforeEach + fun setup() { + // Stubbing isHostInDenylist() so it doesn't attempt to resolve hosts that don't exist in the unit tests + mockkStatic("org.opensearch.notifications.spi.utils.ValidationHelpersKt") + every { org.opensearch.notifications.spi.utils.isHostInDenylist(any(), any()) } returns false + } + + @Test + fun `test MicrosoftTeams message null entity response`() { + val mockHttpClient: CloseableHttpClient = EasyMock.createMock(CloseableHttpClient::class.java) + + // The DestinationHttpClient replaces a null entity with "{}". + val expectedWebhookResponse = DestinationMessageResponse(RestStatus.OK.status, "{}") + + val httpResponse = mockk() + EasyMock.expect(mockHttpClient.execute(EasyMock.anyObject(HttpPost::class.java))).andReturn(httpResponse) + + every { httpResponse.code } returns RestStatus.OK.status + every { httpResponse.entity } returns null + EasyMock.replay(mockHttpClient) + + val httpClient = DestinationHttpClient(mockHttpClient) + val webhookDestinationTransport = WebhookDestinationTransport(httpClient) + DestinationTransportProvider.destinationTransportMap = mapOf(DestinationType.MICROSOFT_TEAMS to webhookDestinationTransport) + + val title = "test MicrosoftTeams" + val messageText = "Message gughjhjlkh Body emoji test: :) :+1: " + + "link test: http://sample.com email test: marymajor@example.com All member callout: " + + "@All All Present member callout: @Present" + val url = "https://abc/com" + + val destination = MicrosoftTeamsDestination(url) + val message = MessageContent(title, messageText) + + val actualMicrosoftTeamsResponse: DestinationMessageResponse = NotificationCoreImpl.sendMessage(destination, message, "ref") + + assertEquals(expectedWebhookResponse.statusText, actualMicrosoftTeamsResponse.statusText) + assertEquals(expectedWebhookResponse.statusCode, actualMicrosoftTeamsResponse.statusCode) + } + + @Test + fun `test MicrosoftTeams message empty entity response`() { + val mockHttpClient: CloseableHttpClient = EasyMock.createMock(CloseableHttpClient::class.java) + val expectedWebhookResponse = DestinationMessageResponse(RestStatus.OK.status, "{}") + + val httpResponse = mockk() + EasyMock.expect(mockHttpClient.execute(EasyMock.anyObject(HttpPost::class.java))).andReturn(httpResponse) + every { httpResponse.code } returns RestStatus.OK.status + every { httpResponse.entity } returns StringEntity("") + EasyMock.replay(mockHttpClient) + + val httpClient = DestinationHttpClient(mockHttpClient) + val webhookDestinationTransport = WebhookDestinationTransport(httpClient) + DestinationTransportProvider.destinationTransportMap = mapOf(DestinationType.MICROSOFT_TEAMS to webhookDestinationTransport) + + val title = "test MicrosoftTeams" + val messageText = "{\"Content\":\"Message gughjhjlkh Body emoji test: :) :+1: " + + "link test: http://sample.com email test: marymajor@example.com All member callout: " + + "@All All Present member callout: @Present\"}" + val url = "https://abc/com" + + val destination = MicrosoftTeamsDestination(url) + val message = MessageContent(title, messageText) + + val actualMicrosoftTeamsResponse: DestinationMessageResponse = NotificationCoreImpl.sendMessage(destination, message, "ref") + + assertEquals(expectedWebhookResponse.statusText, actualMicrosoftTeamsResponse.statusText) + assertEquals(expectedWebhookResponse.statusCode, actualMicrosoftTeamsResponse.statusCode) + } + + @Test + fun `test MicrosoftTeams message non-empty entity response`() { + val responseContent = "It worked!" + val mockHttpClient: CloseableHttpClient = EasyMock.createMock(CloseableHttpClient::class.java) + val expectedWebhookResponse = DestinationMessageResponse(RestStatus.OK.status, responseContent) + + val httpResponse = mockk() + EasyMock.expect(mockHttpClient.execute(EasyMock.anyObject(HttpPost::class.java))).andReturn(httpResponse) + every { httpResponse.code } returns RestStatus.OK.status + every { httpResponse.entity } returns StringEntity(responseContent) + EasyMock.replay(mockHttpClient) + + val httpClient = DestinationHttpClient(mockHttpClient) + val webhookDestinationTransport = WebhookDestinationTransport(httpClient) + DestinationTransportProvider.destinationTransportMap = mapOf(DestinationType.MICROSOFT_TEAMS to webhookDestinationTransport) + + val title = "test MicrosoftTeams" + val messageText = "{\"Content\":\"Message gughjhjlkh Body emoji test: :) :+1: " + + "link test: http://sample.com email test: marymajor@example.com All member callout: " + + "@All All Present member callout: @Present\"}" + val url = "https://abc/com" + + val destination = MicrosoftTeamsDestination(url) + val message = MessageContent(title, messageText) + + val actualMicrosoftTeamsResponse: DestinationMessageResponse = NotificationCoreImpl.sendMessage(destination, message, "ref") + + assertEquals(expectedWebhookResponse.statusText, actualMicrosoftTeamsResponse.statusText) + assertEquals(expectedWebhookResponse.statusCode, actualMicrosoftTeamsResponse.statusCode) + } + + @Test + fun `test url missing should throw IllegalArgumentException with message`() { + val exception = Assertions.assertThrows(IllegalArgumentException::class.java) { + MicrosoftTeamsDestination("") + } + assertEquals("url is null or empty", exception.message) + } + + @Test + fun testUrlInvalidMessage() { + assertThrows { + ChimeDestination("invalidUrl") + } + } + + @ParameterizedTest + @MethodSource("escapeSequenceToRaw") + fun `test build webhook request body for microsoft teams should have title included and prevent escape`( + escapeSequence: String, + rawString: String + ) { + val httpClient = DestinationHttpClient() + val title = "test MicrosoftTeams" + val messageText = "line1${escapeSequence}line2" + val url = "https://abc/com" + val expectedRequestBody = """{"text":"$title\n\nline1${rawString}line2"}""" + val destination = MicrosoftTeamsDestination(url) + val message = MessageContent(title, messageText) + val actualRequestBody = httpClient.buildRequestBody(destination, message) + assertEquals(expectedRequestBody, actualRequestBody) + } +} diff --git a/notifications/core/src/test/kotlin/org/opensearch/notifications/core/settings/PluginSettingsTests.kt b/notifications/core/src/test/kotlin/org/opensearch/notifications/core/settings/PluginSettingsTests.kt index 098da401..147516d9 100644 --- a/notifications/core/src/test/kotlin/org/opensearch/notifications/core/settings/PluginSettingsTests.kt +++ b/notifications/core/src/test/kotlin/org/opensearch/notifications/core/settings/PluginSettingsTests.kt @@ -54,6 +54,7 @@ internal class PluginSettingsTests { listOf( "slack", "chime", + "microsoft_teams", "webhook", "email", "sns", diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt index 21242476..741f8e79 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigIndexingActions.kt @@ -4,7 +4,6 @@ */ package org.opensearch.notifications.index - import org.opensearch.OpenSearchStatusException import org.opensearch.commons.authuser.User import org.opensearch.commons.notifications.action.CreateNotificationConfigRequest @@ -23,6 +22,7 @@ import org.opensearch.commons.notifications.model.Chime import org.opensearch.commons.notifications.model.ConfigType import org.opensearch.commons.notifications.model.Email import org.opensearch.commons.notifications.model.EmailGroup +import org.opensearch.commons.notifications.model.MicrosoftTeams import org.opensearch.commons.notifications.model.NotificationConfig import org.opensearch.commons.notifications.model.NotificationConfigInfo import org.opensearch.commons.notifications.model.NotificationConfigSearchResult @@ -39,7 +39,6 @@ import org.opensearch.notifications.model.DocMetadata import org.opensearch.notifications.model.NotificationConfigDoc import org.opensearch.notifications.security.UserAccess import java.time.Instant - /** * NotificationConfig indexing operation actions. */ @@ -65,6 +64,10 @@ object ConfigIndexingActions { // TODO: URL validation with rules } + private fun validateMicrosoftTeamsConfig(microsoftTeams: MicrosoftTeams, user: User?) { + require(microsoftTeams.url.contains(Regex("https://.*\\.webhook\\.office\\.com"))) + } + @Suppress("UnusedPrivateMember") private fun validateWebhookConfig(webhook: Webhook, user: User?) { // TODO: URL validation with rules @@ -162,6 +165,7 @@ object ConfigIndexingActions { ) ConfigType.SLACK -> validateSlackConfig(config.configData as Slack, user) ConfigType.CHIME -> validateChimeConfig(config.configData as Chime, user) + ConfigType.MICROSOFT_TEAMS -> validateMicrosoftTeamsConfig(config.configData as MicrosoftTeams, user) ConfigType.WEBHOOK -> validateWebhookConfig(config.configData as Webhook, user) ConfigType.EMAIL -> validateEmailConfig(config.configData as Email, user) ConfigType.SMTP_ACCOUNT -> validateSmtpAccountConfig(config.configData as SmtpAccount, user) @@ -369,6 +373,7 @@ object ConfigIndexingActions { return listOf( ConfigType.SLACK.tag, ConfigType.CHIME.tag, + ConfigType.MICROSOFT_TEAMS.tag, ConfigType.WEBHOOK.tag, ConfigType.EMAIL.tag, ConfigType.SNS.tag diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigQueryHelper.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigQueryHelper.kt index 93049399..bdccce08 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigQueryHelper.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/ConfigQueryHelper.kt @@ -28,6 +28,7 @@ import org.opensearch.commons.notifications.NotificationConstants.URL_TAG import org.opensearch.commons.notifications.model.ConfigType.CHIME import org.opensearch.commons.notifications.model.ConfigType.EMAIL import org.opensearch.commons.notifications.model.ConfigType.EMAIL_GROUP +import org.opensearch.commons.notifications.model.ConfigType.MICROSOFT_TEAMS import org.opensearch.commons.notifications.model.ConfigType.SES_ACCOUNT import org.opensearch.commons.notifications.model.ConfigType.SLACK import org.opensearch.commons.notifications.model.ConfigType.SMTP_ACCOUNT @@ -69,6 +70,7 @@ object ConfigQueryHelper { "$DESCRIPTION_TAG.$KEYWORD_SUFFIX", "${SLACK.tag}.$URL_TAG.$KEYWORD_SUFFIX", "${CHIME.tag}.$URL_TAG.$KEYWORD_SUFFIX", + "${MICROSOFT_TEAMS.tag}.$URL_TAG.$KEYWORD_SUFFIX", "${WEBHOOK.tag}.$URL_TAG.$KEYWORD_SUFFIX", "${SMTP_ACCOUNT.tag}.$HOST_TAG.$KEYWORD_SUFFIX", "${SMTP_ACCOUNT.tag}.$FROM_ADDRESS_TAG.$KEYWORD_SUFFIX", @@ -82,6 +84,7 @@ object ConfigQueryHelper { DESCRIPTION_TAG, "${SLACK.tag}.$URL_TAG", "${CHIME.tag}.$URL_TAG", + "${MICROSOFT_TEAMS.tag}.$URL_TAG", "${WEBHOOK.tag}.$URL_TAG", "${SMTP_ACCOUNT.tag}.$HOST_TAG", "${SMTP_ACCOUNT.tag}.$FROM_ADDRESS_TAG", diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/Metrics.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/Metrics.kt index 91c93178..b27cd353 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/Metrics.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/metrics/Metrics.kt @@ -185,6 +185,10 @@ enum class Metrics(val metricName: String, val counter: Counter<*>) { "notifications.message_destination.chime", BasicCounter() ), + NOTIFICATIONS_MESSAGE_DESTINATION_MICROSOFT_TEAMS( + "notifications.message_destination.microsoft_teams", + BasicCounter() + ), NOTIFICATIONS_MESSAGE_DESTINATION_WEBHOOK( "notifications.message_destination.webhook", BasicCounter() diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationConfigRestHandler.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationConfigRestHandler.kt index de8e4a48..e83e797e 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationConfigRestHandler.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/resthandler/NotificationConfigRestHandler.kt @@ -116,6 +116,7 @@ internal class NotificationConfigRestHandler : PluginBaseHandler() { * email_group.recipient_list.recipient=abc,xyz (Text filter field) * slack.url=domain (Text filter field) * chime.url=domain (Text filter field) + * microsoft_teams.url=domain (Text filter field) * webhook.url=domain (Text filter field) * smtp_account.host=domain (Text filter field) * smtp_account.from_address=abc,xyz (Text filter field) diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/send/SendMessageActionHelper.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/send/SendMessageActionHelper.kt index 19b0f2f8..083c01b0 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/send/SendMessageActionHelper.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/send/SendMessageActionHelper.kt @@ -29,6 +29,7 @@ import org.opensearch.commons.notifications.model.EmailGroup import org.opensearch.commons.notifications.model.EmailRecipientStatus import org.opensearch.commons.notifications.model.EventSource import org.opensearch.commons.notifications.model.EventStatus +import org.opensearch.commons.notifications.model.MicrosoftTeams import org.opensearch.commons.notifications.model.NotificationEvent import org.opensearch.commons.notifications.model.SesAccount import org.opensearch.commons.notifications.model.Slack @@ -49,6 +50,7 @@ import org.opensearch.notifications.spi.model.MessageContent import org.opensearch.notifications.spi.model.destination.BaseDestination import org.opensearch.notifications.spi.model.destination.ChimeDestination import org.opensearch.notifications.spi.model.destination.CustomWebhookDestination +import org.opensearch.notifications.spi.model.destination.MicrosoftTeamsDestination import org.opensearch.notifications.spi.model.destination.SesDestination import org.opensearch.notifications.spi.model.destination.SlackDestination import org.opensearch.notifications.spi.model.destination.SmtpDestination @@ -227,6 +229,7 @@ object SendMessageActionHelper { ConfigType.NONE -> null ConfigType.SLACK -> sendSlackMessage(configData as Slack, message, eventStatus, eventSource.referenceId) ConfigType.CHIME -> sendChimeMessage(configData as Chime, message, eventStatus, eventSource.referenceId) + ConfigType.MICROSOFT_TEAMS -> sendMicrosoftTeamsMessage(configData as MicrosoftTeams, message, eventStatus, eventSource.referenceId) ConfigType.WEBHOOK -> sendWebhookMessage( configData as Webhook, message, @@ -390,6 +393,21 @@ object SendMessageActionHelper { return eventStatus.copy(deliveryStatus = DeliveryStatus(status.statusCode.toString(), status.statusText)) } + /** + * send message to Microsoft Teams destination + */ + private fun sendMicrosoftTeamsMessage( + microsoftTeams: MicrosoftTeams, + message: MessageContent, + eventStatus: EventStatus, + referenceId: String + ): EventStatus { + Metrics.NOTIFICATIONS_MESSAGE_DESTINATION_MICROSOFT_TEAMS.counter.increment() + val destination = MicrosoftTeamsDestination(microsoftTeams.url) + val status = sendMessageThroughSpi(destination, message, referenceId) + return eventStatus.copy(deliveryStatus = DeliveryStatus(status.statusCode.toString(), status.statusText)) + } + /** * send message to custom webhook destination */ @@ -585,7 +603,6 @@ object SendMessageActionHelper { DestinationMessageResponse(RestStatus.FAILED_DEPENDENCY.status, "Failed to send notification") } } - /** * Collects all child configs of the channel configurations (like email) * @param channels list of NotificationConfigDocInfo diff --git a/notifications/notifications/src/main/resources/notifications-config-mapping.yml b/notifications/notifications/src/main/resources/notifications-config-mapping.yml index 133fc28c..073d0f98 100644 --- a/notifications/notifications/src/main/resources/notifications-config-mapping.yml +++ b/notifications/notifications/src/main/resources/notifications-config-mapping.yml @@ -8,7 +8,7 @@ # "dynamic" is set to "false" so that only specified fields are indexed instead of all fields. dynamic: false _meta: - schema_version: 1 + schema_version: 2 properties: metadata: type: object @@ -54,6 +54,14 @@ properties: fields: keyword: type: keyword + microsoft_teams: # Microsoft Teams configuration + type: object + properties: + url: + type: text + fields: + keyword: + type: keyword webhook: # webhook configuration type: object properties: @@ -134,4 +142,4 @@ properties: type: text fields: keyword: - type: keyword + type: keyword \ No newline at end of file diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/IntegTestHelpers.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/IntegTestHelpers.kt index 885b16d4..fefc8a61 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/IntegTestHelpers.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/IntegTestHelpers.kt @@ -121,6 +121,9 @@ fun getCreateNotificationRequestJsonString( ConfigType.CHIME -> """ "chime":{"url":"https://chime.domain.com/sample_chime_url#$randomString"} """.trimIndent() + ConfigType.MICROSOFT_TEAMS -> """ + "microsoft_teams":{"url":"https://microsoftTeams.domain.webhook.office.com/sample_microsoft_teams_url#$randomString"} + """.trimIndent() ConfigType.WEBHOOK -> """ "webhook":{"url":"https://web.domain.com/sample_web_url#$randomString"} """.trimIndent() diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SecurityNotificationIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SecurityNotificationIT.kt index 504acdb6..e2d89769 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SecurityNotificationIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SecurityNotificationIT.kt @@ -361,6 +361,7 @@ class SecurityNotificationIT : PluginRestTestCase() { val slackId = createConfig(configType = ConfigType.SLACK) val chimeId = createConfig(configType = ConfigType.CHIME) + val microsoftTeamsId = createConfig(configType = ConfigType.MICROSOFT_TEAMS) val webhookId = createConfig(configType = ConfigType.WEBHOOK) val emailGroupId = createConfig(configType = ConfigType.EMAIL_GROUP) val smtpAccountId = createConfig(configType = ConfigType.SMTP_ACCOUNT) @@ -371,7 +372,7 @@ class SecurityNotificationIT : PluginRestTestCase() { ) Thread.sleep(1000) - val channelIds = setOf(slackId, chimeId, webhookId, emailId) + val channelIds = setOf(slackId, chimeId, microsoftTeamsId, webhookId, emailId) val response = executeRequest( RestRequest.Method.GET.name, "${NotificationPlugin.PLUGIN_BASE_URI}/channels", diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/bwc/NotificationsBackwardsCompatibilityIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/bwc/NotificationsBackwardsCompatibilityIT.kt index 5e808c78..6e7eee5f 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/bwc/NotificationsBackwardsCompatibilityIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/bwc/NotificationsBackwardsCompatibilityIT.kt @@ -5,6 +5,7 @@ package org.opensearch.integtest.bwc +import org.junit.Assert import org.opensearch.common.settings.Settings import org.opensearch.core.rest.RestStatus import org.opensearch.integtest.PluginRestTestCase @@ -52,6 +53,8 @@ class NotificationsBackwardsCompatibilityIT : PluginRestTestCase() { } ClusterType.UPGRADED -> { verifyConfigsExist(setOf(configId)) + createTestNotificationsConfig(randomAlphaOfLength(10)) + Assert.assertEquals(2, getCurrentMappingsSchemaVersion()) } } break diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/CreateNotificationConfigIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/CreateNotificationConfigIT.kt index 50e5baa9..64805e9d 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/CreateNotificationConfigIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/CreateNotificationConfigIT.kt @@ -12,6 +12,7 @@ import org.opensearch.client.WarningFailureException import org.opensearch.commons.notifications.model.Chime import org.opensearch.commons.notifications.model.ConfigType import org.opensearch.commons.notifications.model.MethodType +import org.opensearch.commons.notifications.model.MicrosoftTeams import org.opensearch.commons.notifications.model.NotificationConfig import org.opensearch.commons.notifications.model.Slack import org.opensearch.commons.notifications.model.SmtpAccount @@ -19,7 +20,6 @@ import org.opensearch.commons.notifications.model.Webhook import org.opensearch.core.rest.RestStatus import org.opensearch.integtest.PluginRestTestCase import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI -import org.opensearch.notifications.index.NotificationConfigIndex import org.opensearch.notifications.verifySingleConfigEquals import org.opensearch.rest.RestRequest @@ -103,6 +103,46 @@ class CreateNotificationConfigIT : PluginRestTestCase() { verifySingleConfigEquals(configId, referenceObject, getConfigResponse) } + fun `test Create microsoft teams notification config with ID`() { + // Create sample config request reference + val configId = "sample_config_id" + val sampleMicrosoftTeams = MicrosoftTeams("https://domain.webhook.office.com/1234567") + val referenceObject = NotificationConfig( + "this is a sample config name", + "this is a sample config description", + ConfigType.MICROSOFT_TEAMS, + isEnabled = true, + configData = sampleMicrosoftTeams + ) + + // Create Microsoft Teams notification config + val createRequestJsonString = """ + { + "config_id":"$configId", + "config":{ + "name":"${referenceObject.name}", + "description":"${referenceObject.description}", + "config_type":"microsoft_teams", + "is_enabled":${referenceObject.isEnabled}, + "microsoft_teams":{"url":"${(referenceObject.configData as MicrosoftTeams).url}"} + } + } + """.trimIndent() + val createdConfigId = createConfigWithRequestJsonString(createRequestJsonString) + Assert.assertEquals(configId, createdConfigId) + Thread.sleep(1000) + + // Get Microsoft Teams notification config + + val getConfigResponse = executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/configs/$configId", + "", + RestStatus.OK.status + ) + verifySingleConfigEquals(configId, referenceObject, getConfigResponse) + } + fun `test Create webhook notification config with existing ID fails`() { // Create sample config request reference val sampleWebhook = Webhook("https://domain.com/sample_webhook_url#1234567890") @@ -274,7 +314,7 @@ class CreateNotificationConfigIT : PluginRestTestCase() { Assert.assertEquals(0, getCurrentMappingsSchemaVersion()) createConfig() - Assert.assertEquals(1, getCurrentMappingsSchemaVersion()) + Assert.assertEquals(2, getCurrentMappingsSchemaVersion()) } fun `test _meta field not exists in current mappings`() { @@ -324,13 +364,8 @@ class CreateNotificationConfigIT : PluginRestTestCase() { assert(e is WarningFailureException) } - val getMappingRequest = Request(RestRequest.Method.GET.name, "$indexName/_mappings") - val responseBefore = executeRequest(getMappingRequest, RestStatus.OK.status, client()) - val mappingsObjectBefore = responseBefore.get(indexName).asJsonObject.get("mappings").asJsonObject - Assert.assertNull("mappings should not have _meta field", mappingsObjectBefore.get(NotificationConfigIndex._META)) + Assert.assertEquals(1, getCurrentMappingsSchemaVersion()) createConfig() - val responseAfter = executeRequest(getMappingRequest, RestStatus.OK.status, client()) - val mappingsObjectAfter = responseAfter.get(indexName).asJsonObject.get("mappings").asJsonObject - Assert.assertEquals(mappingsObjectAfter, mappingsObjectBefore) + Assert.assertEquals(2, getCurrentMappingsSchemaVersion()) } } diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/MicrosoftTeamsNotificationConfigCrudIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/MicrosoftTeamsNotificationConfigCrudIT.kt new file mode 100644 index 00000000..2707d0c6 --- /dev/null +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/MicrosoftTeamsNotificationConfigCrudIT.kt @@ -0,0 +1,158 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.integtest.config + +import org.junit.Assert +import org.opensearch.commons.notifications.model.ConfigType +import org.opensearch.commons.notifications.model.MicrosoftTeams +import org.opensearch.commons.notifications.model.NotificationConfig +import org.opensearch.core.rest.RestStatus +import org.opensearch.integtest.PluginRestTestCase +import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI +import org.opensearch.notifications.verifySingleConfigEquals +import org.opensearch.rest.RestRequest + +class MicrosoftTeamsNotificationConfigCrudIT : PluginRestTestCase() { + + fun `test Create, Get, Update, Delete microsoft teams notification config using REST client`() { + // Create sample config request reference + val sampleMicrosoftTeams = MicrosoftTeams("https://domain.webhook.office.com/webhook2/test") + val referenceObject = NotificationConfig( + "this is a sample config name", + "this is a sample config description", + ConfigType.MICROSOFT_TEAMS, + isEnabled = true, + configData = sampleMicrosoftTeams + ) + + // Create Microsoft Teams notification config + val createRequestJsonString = """ + { + "config":{ + "name":"${referenceObject.name}", + "description":"${referenceObject.description}", + "config_type":"microsoft_teams", + "is_enabled":${referenceObject.isEnabled}, + "microsoft_teams":{"url":"${(referenceObject.configData as MicrosoftTeams).url}"} + } + } + """.trimIndent() + val configId = createConfigWithRequestJsonString(createRequestJsonString) + Assert.assertNotNull(configId) + Thread.sleep(1000) + + // Get Microsoft Teams notification config + + val getConfigResponse = executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/configs/$configId", + "", + RestStatus.OK.status + ) + verifySingleConfigEquals(configId, referenceObject, getConfigResponse) + Thread.sleep(100) + + // Get all notification config + + val getAllConfigResponse = executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/configs", + "", + RestStatus.OK.status + ) + verifySingleConfigEquals(configId, referenceObject, getAllConfigResponse) + Thread.sleep(100) + + // Updated notification config object + val updatedMicrosoftTeams = MicrosoftTeams("https://updated.domain.webhook.office.com/webhook2/test") + val updatedObject = NotificationConfig( + "this is a updated config name", + "this is a updated config description", + ConfigType.MICROSOFT_TEAMS, + isEnabled = true, + configData = updatedMicrosoftTeams + ) + + // Update Microsoft Teams notification config + val updateRequestJsonString = """ + { + "config":{ + "name":"${updatedObject.name}", + "description":"${updatedObject.description}", + "config_type":"microsoft_teams", + "is_enabled":${updatedObject.isEnabled}, + "microsoft_teams":{"url":"${(updatedObject.configData as MicrosoftTeams).url}"} + } + } + """.trimIndent() + val updateResponse = executeRequest( + RestRequest.Method.PUT.name, + "$PLUGIN_BASE_URI/configs/$configId", + updateRequestJsonString, + RestStatus.OK.status + ) + Assert.assertEquals(configId, updateResponse.get("config_id").asString) + Thread.sleep(1000) + + // Get updated Microsoft Teams notification config + + val getUpdatedConfigResponse = executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/configs/$configId", + "", + RestStatus.OK.status + ) + verifySingleConfigEquals(configId, updatedObject, getUpdatedConfigResponse) + Thread.sleep(100) + + // Delete Microsoft Teams notification config + val deleteResponse = deleteConfig(configId) + Assert.assertEquals("OK", deleteResponse.get("delete_response_list").asJsonObject.get(configId).asString) + Thread.sleep(1000) + + // Get Microsoft Teams notification config after delete + + executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/configs/$configId", + "", + RestStatus.NOT_FOUND.status + ) + Thread.sleep(100) + } + + fun `test Bad Request for multiple config data for microsoft teams using REST Client`() { + // Create sample config request reference + val sampleMicrosoftTeams = MicrosoftTeams("https://domain.webhook.office.com/1234567") + val referenceObject = NotificationConfig( + "this is a sample config name", + "this is a sample config description", + ConfigType.MICROSOFT_TEAMS, + isEnabled = true, + configData = sampleMicrosoftTeams + ) + + // Create Microsoft Teams notification config + val createRequestJsonString = """ + { + "config":{ + "name":"${referenceObject.name}", + "description":"${referenceObject.description}", + "config_type":"microsoft_teams", + "is_enabled":${referenceObject.isEnabled}, + "chime":{"url":"https://dummy.com"} + "microsoft_teams":{"url":"${(referenceObject.configData as MicrosoftTeams).url}"} + } + } + """.trimIndent() + executeRequest( + RestRequest.Method.POST.name, + "$PLUGIN_BASE_URI/configs", + createRequestJsonString, + RestStatus.BAD_REQUEST.status + ) + } +} diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/QueryNotificationConfigIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/QueryNotificationConfigIT.kt index a926463d..47efbe32 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/QueryNotificationConfigIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/QueryNotificationConfigIT.kt @@ -261,12 +261,13 @@ class QueryNotificationConfigIT : PluginRestTestCase() { fun `test Get sorted notification config using single keyword sort_field(config_type)`() { val slackId = createConfig(configType = ConfigType.SLACK) val chimeId = createConfig(configType = ConfigType.CHIME) + val microsoftTeamsId = createConfig(configType = ConfigType.MICROSOFT_TEAMS) val webhookId = createConfig(configType = ConfigType.WEBHOOK) val emailGroupId = createConfig(configType = ConfigType.EMAIL_GROUP) val smtpAccountId = createConfig(configType = ConfigType.SMTP_ACCOUNT) Thread.sleep(1000) - val sortedConfigIds = listOf(chimeId, emailGroupId, slackId, smtpAccountId, webhookId) + val sortedConfigIds = listOf(chimeId, emailGroupId, microsoftTeamsId, slackId, smtpAccountId, webhookId) // Get all notification configs with default sort_order(asc) val getDefaultOrderConfigResponse = executeRequest( @@ -338,6 +339,7 @@ class QueryNotificationConfigIT : PluginRestTestCase() { fun `test Get filtered notification config using keyword filter_param_list(config_type)`() { val slackId = createConfig(configType = ConfigType.SLACK) val chimeId = createConfig(configType = ConfigType.CHIME) + val microsoftTeamsId = createConfig(configType = ConfigType.MICROSOFT_TEAMS) val webhookId = createConfig(configType = ConfigType.WEBHOOK) val emailGroupId = createConfig(configType = ConfigType.EMAIL_GROUP) val smtpAccountId = createConfig(configType = ConfigType.SMTP_ACCOUNT) @@ -354,13 +356,13 @@ class QueryNotificationConfigIT : PluginRestTestCase() { Thread.sleep(100) // Get notification configs with 2 item type - val getSlackOrChimeResponse = executeRequest( + val getMicrosoftTeamsOrChimeResponse = executeRequest( RestRequest.Method.GET.name, - "$PLUGIN_BASE_URI/configs?config_type=slack,chime", + "$PLUGIN_BASE_URI/configs?config_type=microsoft_teams,chime", "", RestStatus.OK.status ) - verifyMultiConfigIdEquals(setOf(slackId, chimeId), getSlackOrChimeResponse, 2) + verifyMultiConfigIdEquals(setOf(microsoftTeamsId, chimeId), getMicrosoftTeamsOrChimeResponse, 2) Thread.sleep(100) // Get notification configs with 3 item type @@ -547,6 +549,7 @@ class QueryNotificationConfigIT : PluginRestTestCase() { fun `test Get filtered notification config using keyword filter_param_list(internal config fields)`() { val slackId = createConfig(configType = ConfigType.SLACK) val chimeId = createConfig(configType = ConfigType.CHIME) + val microsoftTeamsId = createConfig(configType = ConfigType.MICROSOFT_TEAMS) val webhookId = createConfig(configType = ConfigType.WEBHOOK) val emailGroupId = createConfig(configType = ConfigType.EMAIL_GROUP) val smtpAccountId = createConfig(configType = ConfigType.SMTP_ACCOUNT) @@ -572,6 +575,16 @@ class QueryNotificationConfigIT : PluginRestTestCase() { verifySingleConfigIdEquals(chimeId, getChimeResponse, 1) Thread.sleep(100) + // Get notification configs using microsoft_teams.url + val getMicrosoftTeamsResponse = executeRequest( + RestRequest.Method.GET.name, + "$PLUGIN_BASE_URI/configs?microsoft_teams.url=$microsoftTeamsId", + "", + RestStatus.OK.status + ) + verifySingleConfigIdEquals(microsoftTeamsId, getMicrosoftTeamsResponse, 1) + Thread.sleep(100) + // Get notification configs using webhook.url val getWebhookResponse = executeRequest( RestRequest.Method.GET.name, @@ -606,14 +619,15 @@ class QueryNotificationConfigIT : PluginRestTestCase() { fun `test Get filtered notification config using query`() { val slackId = createConfig(configType = ConfigType.SLACK) val chimeId = createConfig(configType = ConfigType.CHIME) + val microsoftTeamsId = createConfig(configType = ConfigType.MICROSOFT_TEAMS) val webhookId = createConfig(configType = ConfigType.WEBHOOK) val emailGroupId = createConfig(configType = ConfigType.EMAIL_GROUP) val smtpAccountId = createConfig(configType = ConfigType.SMTP_ACCOUNT) - val allIds = setOf(slackId, chimeId, webhookId, emailGroupId, smtpAccountId) - val urlIds = setOf(slackId, chimeId, webhookId) + val allIds = setOf(slackId, chimeId, microsoftTeamsId, webhookId, emailGroupId, smtpAccountId) + val urlIds = setOf(slackId, chimeId, microsoftTeamsId, webhookId) val recipientIds = setOf(emailGroupId) val fromIds = setOf(emailGroupId, smtpAccountId) - val domainIds = setOf(slackId, chimeId, webhookId, smtpAccountId) + val domainIds = setOf(slackId, chimeId, microsoftTeamsId, webhookId, smtpAccountId) Thread.sleep(1000) // Get notification configs using query=slack @@ -680,14 +694,15 @@ class QueryNotificationConfigIT : PluginRestTestCase() { fun `test Get filtered notification config using text_query`() { val slackId = createConfig(configType = ConfigType.SLACK) val chimeId = createConfig(configType = ConfigType.CHIME) + val microsoftTeamsId = createConfig(configType = ConfigType.MICROSOFT_TEAMS) val webhookId = createConfig(configType = ConfigType.WEBHOOK) val emailGroupId = createConfig(configType = ConfigType.EMAIL_GROUP) val smtpAccountId = createConfig(configType = ConfigType.SMTP_ACCOUNT) - val allIds = setOf(slackId, chimeId, webhookId, emailGroupId, smtpAccountId) - val urlIds = setOf(slackId, chimeId, webhookId) + val allIds = setOf(slackId, chimeId, microsoftTeamsId, webhookId, emailGroupId, smtpAccountId) + val urlIds = setOf(slackId, chimeId, microsoftTeamsId, webhookId) val recipientIds = setOf(emailGroupId) val fromIds = setOf(emailGroupId, smtpAccountId) - val domainIds = setOf(slackId, chimeId, webhookId, smtpAccountId) + val domainIds = setOf(slackId, chimeId, microsoftTeamsId, webhookId, smtpAccountId) Thread.sleep(1000) // Get notification configs using text_query=slack should not return any item diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/SlackNotificationConfigCrudIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/SlackNotificationConfigCrudIT.kt index 2b51f39a..86c64cf2 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/SlackNotificationConfigCrudIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/config/SlackNotificationConfigCrudIT.kt @@ -144,7 +144,7 @@ class SlackNotificationConfigCrudIT : PluginRestTestCase() { "config_type":"slack", "is_enabled":${referenceObject.isEnabled}, "chime":{"url":"https://dummy.com"} - "slack":{"url":"${(referenceObject.configData as Slack).url}"} + "slack":{"url":"${(referenceObject.configData as Slack).url} } } """.trimIndent() diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/features/GetNotificationChannelListIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/features/GetNotificationChannelListIT.kt index 339ea038..283f7556 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/features/GetNotificationChannelListIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/features/GetNotificationChannelListIT.kt @@ -35,6 +35,7 @@ class GetNotificationChannelListIT : PluginRestTestCase() { fun `test getChannelList should return only channels`() { val slackId = createConfig(configType = ConfigType.SLACK) val chimeId = createConfig(configType = ConfigType.CHIME) + val microsoftTeamsId = createConfig(configType = ConfigType.MICROSOFT_TEAMS) val webhookId = createConfig(configType = ConfigType.WEBHOOK) val emailGroupId = createConfig(configType = ConfigType.EMAIL_GROUP) val smtpAccountId = createConfig(configType = ConfigType.SMTP_ACCOUNT) @@ -45,7 +46,7 @@ class GetNotificationChannelListIT : PluginRestTestCase() { ) Thread.sleep(1000) - val channelIds = setOf(slackId, chimeId, webhookId, emailId) + val channelIds = setOf(slackId, chimeId, microsoftTeamsId, webhookId, emailId) val response = executeRequest( RestRequest.Method.GET.name, "$PLUGIN_BASE_URI/channels", diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/send/SendTestMessageRestHandlerIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/send/SendTestMessageRestHandlerIT.kt index bb1d0949..c36e2666 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/send/SendTestMessageRestHandlerIT.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/send/SendTestMessageRestHandlerIT.kt @@ -46,7 +46,6 @@ internal class SendTestMessageRestHandlerIT : PluginRestTestCase() { val error = sendResponse.get("error").asJsonObject Assert.assertNotNull(error.get("reason").asString) } - @Suppress("EmptyFunctionBlock") fun `test send test slack message`() { // Create webhook notification config @@ -79,6 +78,38 @@ internal class SendTestMessageRestHandlerIT : PluginRestTestCase() { val error = sendResponse.get("error").asJsonObject Assert.assertNotNull(error.get("reason").asString) } + @Suppress("EmptyFunctionBlock") + fun `test send test microsoft teams message`() { + // Create webhook notification config + val createRequestJsonString = """ + { + "config":{ + "name":"this is a sample config name", + "description":"this is a sample config description", + "config_type":"microsoft_teams", + "is_enabled":true, + "microsoft_teams":{ + "url":"https://hooks.webhook.office.com/webhook2/abcde" + } + } + } + """.trimIndent() + val configId = createConfigWithRequestJsonString(createRequestJsonString) + Assert.assertNotNull(configId) + Thread.sleep(1000) + + // send test message + val sendResponse = executeRequest( + RestRequest.Method.POST.name, + "$PLUGIN_BASE_URI/feature/test/$configId", + "", + RestStatus.INTERNAL_SERVER_ERROR.status + ) + + // verify failure response is with message + val error = sendResponse.get("error").asJsonObject + Assert.assertNotNull(error.get("reason").asString) + } @Suppress("EmptyFunctionBlock") fun `test send custom webhook message`() { diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/ObjectEqualsHelpers.kt b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/ObjectEqualsHelpers.kt index 99b4f2cc..98d7e929 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/ObjectEqualsHelpers.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/ObjectEqualsHelpers.kt @@ -14,6 +14,7 @@ import org.opensearch.commons.notifications.model.Email import org.opensearch.commons.notifications.model.EmailGroup import org.opensearch.commons.notifications.model.EmailRecipient import org.opensearch.commons.notifications.model.MethodType +import org.opensearch.commons.notifications.model.MicrosoftTeams import org.opensearch.commons.notifications.model.NotificationConfig import org.opensearch.commons.notifications.model.SesAccount import org.opensearch.commons.notifications.model.Slack @@ -29,6 +30,10 @@ fun verifyEquals(chime: Chime, jsonObject: JsonObject) { Assert.assertEquals(chime.url, jsonObject.get("url").asString) } +fun verifyEquals(microsoftTeams: MicrosoftTeams, jsonObject: JsonObject) { + Assert.assertEquals(microsoftTeams.url, jsonObject.get("url").asString) +} + fun verifyEquals(webhook: Webhook, jsonObject: JsonObject) { Assert.assertEquals(webhook.url, jsonObject.get("url").asString) Assert.assertEquals(webhook.headerParams, Gson().fromJson(jsonObject.get("header_params"), HashMap::class.java)) @@ -83,6 +88,7 @@ fun verifyEquals(config: NotificationConfig, jsonObject: JsonObject) { when (config.configType) { ConfigType.SLACK -> verifyEquals((config.configData as Slack), jsonObject.get("slack").asJsonObject) ConfigType.CHIME -> verifyEquals((config.configData as Chime), jsonObject.get("chime").asJsonObject) + ConfigType.MICROSOFT_TEAMS -> verifyEquals((config.configData as MicrosoftTeams), jsonObject.get("microsoft_teams").asJsonObject) ConfigType.WEBHOOK -> verifyEquals((config.configData as Webhook), jsonObject.get("webhook").asJsonObject) ConfigType.EMAIL -> verifyEquals((config.configData as Email), jsonObject.get("email").asJsonObject) ConfigType.SMTP_ACCOUNT -> verifyEquals( diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/index/ConfigIndexingActionsTests.kt b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/index/ConfigIndexingActionsTests.kt new file mode 100644 index 00000000..d2c6b0ee --- /dev/null +++ b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/index/ConfigIndexingActionsTests.kt @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.opensearch.notifications.index + +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.opensearch.commons.authuser.User +import org.opensearch.commons.notifications.model.MicrosoftTeams +import java.lang.reflect.Method +import kotlin.test.assertFails + +class ConfigIndexingActionsTests { + + @Test + fun `test validate microsoft teams`() { + val user = User() + var microsoftTeams = MicrosoftTeams("https://abcdefg.webhook.office.com/webhookb2/12345567abcdefg") + validateMicrosoftTeamsConfig.invoke(ConfigIndexingActions, microsoftTeams, user) + microsoftTeams = MicrosoftTeams("https://abcde.efg.webhook.office.com/webhookb2/12345567abcdefg") + validateMicrosoftTeamsConfig.invoke(ConfigIndexingActions, microsoftTeams, user) + microsoftTeams = MicrosoftTeams("http://abcdefg.webhook.office.com/webhookb2/12345567abcdefg") + assertFails { validateMicrosoftTeamsConfig.invoke(ConfigIndexingActions, microsoftTeams, user) } + microsoftTeams = MicrosoftTeams("https://abcdefg.webhook.abc.com/webhookb2/12345567abcdefg") + assertFails { validateMicrosoftTeamsConfig.invoke(ConfigIndexingActions, microsoftTeams, user) } + microsoftTeams = MicrosoftTeams("https://abcdefg.abc.com") + assertFails { validateMicrosoftTeamsConfig.invoke(ConfigIndexingActions, microsoftTeams, user) } + } + + companion object { + private lateinit var validateMicrosoftTeamsConfig: Method + + @BeforeAll + @JvmStatic + fun initialize() { + /* use reflection to get private method */ + validateMicrosoftTeamsConfig = ConfigIndexingActions::class.java.getDeclaredMethod( + "validateMicrosoftTeamsConfig", MicrosoftTeams::class.java, User::class.java + ) + + validateMicrosoftTeamsConfig.isAccessible = true + } + } +}