diff --git a/.github/workflows/security-notifications-test-workflow.yml b/.github/workflows/security-notifications-test-workflow.yml new file mode 100644 index 00000000..ec95c7e5 --- /dev/null +++ b/.github/workflows/security-notifications-test-workflow.yml @@ -0,0 +1,92 @@ +## + # Copyright OpenSearch Contributors + # SPDX-License-Identifier: Apache-2.0 +## + +name: Security Test and Build Notifications + +on: [push, pull_request] + +jobs: + build: + strategy: + # This setting says that all jobs should finish, even if one fails + fail-fast: false + matrix: + java: [11, 17] + + runs-on: ubuntu-latest + + steps: + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + + # notifications + - name: Checkout Notifications + uses: actions/checkout@v2 + + # Temporarily exclude tests which causing CI to fail. Tracking in #251 + - name: Build with Gradle + # Only assembling since the full build is governed by other workflows + run: | + cd notifications + ./gradlew assemble + + - name: Pull and Run Docker + run: | + plugin_core=`basename $(ls notifications/core/build/distributions/*.zip)` + plugin=`basename $(ls notifications/notifications/build/distributions/*.zip)` + list_of_files=`ls` + list_of_all_files=`ls notifications/core/build/distributions/` + version=`echo $plugin|awk -F- '{print $3}'| cut -d. -f 1-3` + plugin_version=`echo $plugin|awk -F- '{print $3}'| cut -d. -f 1-4` + qualifier=`echo $plugin|awk -F- '{print $4}'| cut -d. -f 1-1` + candidate_version=`echo $plugin|awk -F- '{print $5}'| cut -d. -f 1-1` + docker_version=$version + + [[ -z $candidate_version ]] && candidate_version=$qualifier && qualifier="" + + echo plugin version plugin_version qualifier candidate_version docker_version + echo "($plugin) ($version) ($plugin_version) ($qualifier) ($candidate_version) ($docker_version)" + echo $ls $list_of_all_files + + if docker pull opensearchstaging/opensearch:$docker_version + then + echo "FROM opensearchstaging/opensearch:$docker_version" >> Dockerfile + echo "RUN /usr/share/opensearch/bin/opensearch-plugin remove opensearch-notifications;" >> Dockerfile + echo "RUN /usr/share/opensearch/bin/opensearch-plugin remove opensearch-notifications-core;" >> Dockerfile + echo "ADD notifications/core/build/distributions/$plugin_core /tmp/" >> Dockerfile + echo "RUN /usr/share/opensearch/bin/opensearch-plugin install --batch file:/tmp/$plugin_core" >> Dockerfile + echo "ADD notifications/notifications/build/distributions/$plugin /tmp/" >> Dockerfile + echo "RUN /usr/share/opensearch/bin/opensearch-plugin install --batch file:/tmp/$plugin" >> Dockerfile + docker build -t opensearch-notifications:test . + echo "imagePresent=true" >> $GITHUB_ENV + else + echo "imagePresent=false" >> $GITHUB_ENV + fi + + - name: Run Docker Image + if: env.imagePresent == 'true' + run: | + cd .. + docker run -p 9200:9200 -d -p 9600:9600 -e "discovery.type=single-node" opensearch-notifications:test + sleep 120 + + - name: Run Notification Test for security enabled test cases + if: env.imagePresent == 'true' + run: | + cluster_running=`curl -XGET https://localhost:9200/_cat/plugins -u admin:admin --insecure` + echo $cluster_running + security=`curl -XGET https://localhost:9200/_cat/plugins -u admin:admin --insecure |grep opensearch-security|wc -l` + echo $security + if [ $security -gt 0 ] + then + echo "Security plugin is available" + cd notifications + ./gradlew integTest -Dtests.rest.cluster=localhost:9200 -Dtests.cluster=localhost:9200 -Dtests.clustername=docker-cluster -Dhttps=true -Duser=admin -Dpassword=admin + else + echo "Security plugin is NOT available skipping this run as tests without security have already been run" + exit 1 + fi \ No newline at end of file diff --git a/notifications/notifications/build.gradle b/notifications/notifications/build.gradle index 0ec7fc9f..f37938d4 100644 --- a/notifications/notifications/build.gradle +++ b/notifications/notifications/build.gradle @@ -208,6 +208,12 @@ integTest { jvmArgs '-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=8000' } + if (System.getProperty("https") == null || System.getProperty("https") == "false") { + filter { + excludeTestsMatching "org.opensearch.*.Security*IT" + } + } + if (System.getProperty("tests.rest.bwcsuite") == null) { filter { excludeTestsMatching "org.opensearch.integtest.bwc.*IT" diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/SendTestNotificationAction.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/SendTestNotificationAction.kt index a09538bf..a2950a4d 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/SendTestNotificationAction.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/action/SendTestNotificationAction.kt @@ -37,7 +37,7 @@ internal class SendTestNotificationAction @Inject constructor( ::SendTestNotificationRequest ) { companion object { - private const val NAME = "cluster:admin/opensearch/notifications/test_notification" + internal const val NAME = "cluster:admin/opensearch/notifications/test_notification" internal val ACTION_TYPE = ActionType(NAME, ::SendNotificationResponse) private val log by logger(SendTestNotificationAction::class.java) } diff --git a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/NotificationConfigIndex.kt b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/NotificationConfigIndex.kt index bc150934..0ea2978c 100644 --- a/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/NotificationConfigIndex.kt +++ b/notifications/notifications/src/main/kotlin/org/opensearch/notifications/index/NotificationConfigIndex.kt @@ -24,6 +24,7 @@ import org.opensearch.action.search.SearchResponse import org.opensearch.client.Client import org.opensearch.cluster.service.ClusterService import org.opensearch.common.unit.TimeValue +import org.opensearch.common.util.concurrent.ThreadContext import org.opensearch.common.xcontent.LoggingDeprecationHandler import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.common.xcontent.XContentHelper @@ -104,18 +105,20 @@ internal object NotificationConfigIndex : ConfigOperations { val request = CreateIndexRequest(INDEX_NAME) .mapping(indexMappingAsMap) .settings(indexSettingsSource, XContentType.YAML) - try { - val response: CreateIndexResponse = client.suspendUntilTimeout(PluginSettings.operationTimeoutMs) { - admin().indices().create(request, it) - } - if (response.isAcknowledged) { - log.info("$LOG_PREFIX:Index $INDEX_NAME creation Acknowledged") - } else { - throw IllegalStateException("$LOG_PREFIX:Index $INDEX_NAME creation not Acknowledged") - } - } catch (exception: Exception) { - if (exception !is ResourceAlreadyExistsException && exception.cause !is ResourceAlreadyExistsException) { - throw exception + client.threadPool().threadContext.stashContext().use { + try { + val response: CreateIndexResponse = client.suspendUntilTimeout(PluginSettings.operationTimeoutMs) { + admin().indices().create(request, it) + } + if (response.isAcknowledged) { + log.info("$LOG_PREFIX:Index $INDEX_NAME creation Acknowledged") + } else { + throw IllegalStateException("$LOG_PREFIX:Index $INDEX_NAME creation not Acknowledged") + } + } catch (exception: Exception) { + if (exception !is ResourceAlreadyExistsException && exception.cause !is ResourceAlreadyExistsException) { + throw exception + } } } } @@ -300,3 +303,40 @@ internal object NotificationConfigIndex : ConfigOperations { return mutableMap } } + +/** + * Executes the given [block] function on this resource and then closes it down correctly whether an exception + * is thrown or not. + * + * In case if the resource is being closed due to an exception occurred in [block], and the closing also fails with an exception, + * the latter is added to the [suppressed][java.lang.Throwable.addSuppressed] exceptions of the former. + * + * @param block a function to process this [AutoCloseable] resource. + * @return the result of [block] function invoked on this resource. + */ +private inline fun T.use(block: (T) -> R): R { + var exception: Throwable? = null + try { + return block(this) + } catch (e: Throwable) { + exception = e + throw e + } finally { + closeFinally(exception) + } +} + +/** + * Closes this [AutoCloseable], suppressing possible exception or error thrown by [AutoCloseable.close] function when + * it's being closed due to some other [cause] exception occurred. + * + * The suppressed exception is added to the list of suppressed exceptions of [cause] exception. + */ +private fun ThreadContext.StoredContext.closeFinally(cause: Throwable?) = when (cause) { + null -> close() + else -> try { + close() + } catch (closeException: Throwable) { + cause.addSuppressed(closeException) + } +} diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/AccessRoles.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/AccessRoles.kt new file mode 100644 index 00000000..851f545e --- /dev/null +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/AccessRoles.kt @@ -0,0 +1,32 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.integtest + +import org.opensearch.commons.notifications.action.NotificationsActions +import org.opensearch.notifications.action.SendTestNotificationAction + +val ALL_ACCESS_ROLE = "all_access" +val NOTIFICATION_FULL_ACCESS_ROLE = "notifications_full_access" +val NOTIFICATION_READ_ONLY_ACCESS = "notifications_read_access" +val NOTIFICATION_NO_ACCESS_ROLE = "no_access" +val NOTIFICATION_CREATE_CONFIG_ACCESS = "notifications_create_config_access" +val NOTIFICATION_UPDATE_CONFIG_ACCESS = "notifications_update_config_access" +val NOTIFICATION_DELETE_CONFIG_ACCESS = "notifications_delete_config_access" +val NOTIFICATION_GET_CONFIG_ACCESS = "notifications_get_config_access" +val NOTIFICATION_GET_PLUGIN_FEATURE_ACCESS = "notifications_get_plugin_access" +val NOTIFICATION_GET_CHANNEL_ACCESS = "notifications_get_channel_access" +val NOTIFICATION_TEST_SEND_ACCESS = "notifications_test_send_access" + +val ROLE_TO_PERMISSION_MAPPING = mapOf( + NOTIFICATION_NO_ACCESS_ROLE to "", + NOTIFICATION_CREATE_CONFIG_ACCESS to NotificationsActions.CREATE_NOTIFICATION_CONFIG_NAME, + NOTIFICATION_UPDATE_CONFIG_ACCESS to NotificationsActions.UPDATE_NOTIFICATION_CONFIG_NAME, + NOTIFICATION_DELETE_CONFIG_ACCESS to NotificationsActions.DELETE_NOTIFICATION_CONFIG_NAME, + NOTIFICATION_GET_CONFIG_ACCESS to NotificationsActions.GET_NOTIFICATION_CONFIG_NAME, + NOTIFICATION_GET_PLUGIN_FEATURE_ACCESS to NotificationsActions.GET_PLUGIN_FEATURES_NAME, + NOTIFICATION_GET_CHANNEL_ACCESS to NotificationsActions.GET_CHANNEL_LIST_NAME, + NOTIFICATION_TEST_SEND_ACCESS to SendTestNotificationAction.NAME +) 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 53830d86..3686f424 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/IntegTestHelpers.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/IntegTestHelpers.kt @@ -9,11 +9,13 @@ import com.google.gson.JsonObject import com.google.gson.JsonParser import org.junit.Assert import org.opensearch.client.Response +import org.opensearch.commons.notifications.model.ConfigType import java.io.BufferedReader import java.io.IOException import java.io.InputStreamReader import java.nio.charset.StandardCharsets import java.time.Instant +import kotlin.random.Random import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -99,6 +101,68 @@ fun getStatusText(response: JsonObject): String { .get("status_text").asString } +private val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') + +fun getCreateNotificationRequestJsonString( + nameSubstring: String, + configType: ConfigType, + isEnabled: Boolean, + smtpAccountId: String = "", + emailGroupId: Set = setOf() +): String { + val randomString = (1..20) + .map { Random.nextInt(0, charPool.size) } + .map(charPool::get) + .joinToString("") + val configObjectString = when (configType) { + ConfigType.SLACK -> """ + "slack":{"url":"https://slack.domain.com/sample_slack_url#$randomString"} + """.trimIndent() + ConfigType.CHIME -> """ + "chime":{"url":"https://chime.domain.com/sample_chime_url#$randomString"} + """.trimIndent() + ConfigType.WEBHOOK -> """ + "webhook":{"url":"https://web.domain.com/sample_web_url#$randomString"} + """.trimIndent() + ConfigType.SMTP_ACCOUNT -> """ + "smtp_account":{ + "host":"smtp.domain.com", + "port":"4321", + "method":"ssl", + "from_address":"$randomString@from.com" + } + """.trimIndent() + ConfigType.EMAIL_GROUP -> """ + "email_group":{ + "recipient_list":[ + {"recipient":"$randomString+recipient1@from.com"}, + {"recipient":"$randomString+recipient2@from.com"} + ] + } + """.trimIndent() + ConfigType.EMAIL -> """ + "email":{ + "email_account_id":"$smtpAccountId", + "recipient_list":[{"recipient":"$randomString@from.com"}], + "email_group_id_list":[${emailGroupId.joinToString { "\"$it\"" }}] + } + """.trimIndent() + else -> throw IllegalArgumentException("Unsupported configType=$configType") + } + return """ + { + "config_id":"$randomString", + "config":{ + "name":"$nameSubstring:this is a sample config name $randomString", + "description":"this is a sample config description $randomString", + "config_type":"$configType", + "is_enabled":$isEnabled, + $configObjectString + } + } + """.trimIndent() +} + /** Util class to build Json entity of request body */ class NotificationsJsonEntity( private val refTag: String?, diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/PluginRestTestCase.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/PluginRestTestCase.kt index deecb5c2..19e2f61b 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/PluginRestTestCase.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/PluginRestTestCase.kt @@ -9,6 +9,7 @@ import com.google.gson.JsonObject import org.apache.http.HttpHost import org.junit.After import org.junit.AfterClass +import org.junit.Assert import org.opensearch.client.Request import org.opensearch.client.RequestOptions import org.opensearch.client.ResponseException @@ -20,7 +21,11 @@ import org.opensearch.common.xcontent.DeprecationHandler import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.common.xcontent.XContentType import org.opensearch.commons.ConfigConstants +import org.opensearch.commons.notifications.model.ConfigType import org.opensearch.commons.rest.SecureRestClientBuilder +import org.opensearch.notifications.NotificationPlugin +import org.opensearch.rest.RestRequest +import org.opensearch.rest.RestStatus import org.opensearch.test.rest.OpenSearchRestTestCase import java.io.IOException import java.nio.file.Files @@ -32,7 +37,7 @@ import javax.management.remote.JMXServiceURL abstract class PluginRestTestCase : OpenSearchRestTestCase() { - private fun isHttps(): Boolean { + protected fun isHttps(): Boolean { return System.getProperty("https", "false")!!.toBoolean() } @@ -127,19 +132,20 @@ abstract class PluginRestTestCase : OpenSearchRestTestCase() { method: String, url: String, jsonString: String, - expectedRestStatus: Int? = null + expectedRestStatus: Int? = null, + client: RestClient = client() ): JsonObject { val request = Request(method, url) request.setJsonEntity(jsonString) val restOptionsBuilder = RequestOptions.DEFAULT.toBuilder() restOptionsBuilder.addHeader("Content-Type", "application/json") request.setOptions(restOptionsBuilder) - return executeRequest(request, expectedRestStatus) + return executeRequest(request, expectedRestStatus, client) } - fun executeRequest(request: Request, expectedRestStatus: Int? = null): JsonObject { + fun executeRequest(request: Request, expectedRestStatus: Int? = null, client: RestClient = client()): JsonObject { val response = try { - client().performRequest(request) + client.performRequest(request) } catch (exception: ResponseException) { exception.response } @@ -150,20 +156,157 @@ abstract class PluginRestTestCase : OpenSearchRestTestCase() { return jsonify(responseBody) } + fun createUser(name: String, passwd: String, backendRoles: Array) { + val request = Request("PUT", "/_plugins/_security/api/internalusers/$name") + val broles = backendRoles.joinToString { it -> "\"$it\"" } + val entity = " {\n" + + "\"password\": \"$passwd\",\n" + + "\"backend_roles\": [$broles],\n" + + "\"attributes\": {\n" + + "}} " + request.setJsonEntity(entity) + client().performRequest(request) + } + + fun deleteUser(name: String) { + val request = Request(RestRequest.Method.DELETE.name, "/_plugins/_security/api/internalusers/$name") + executeRequest(request, RestStatus.OK.status) + } + + fun createUserRolesMapping(role: String, users: Array) { + val request = Request("PUT", "/_plugins/_security/api/rolesmapping/$role") + val usersStr = users.joinToString { it -> "\"$it\"" } + val entity = "{ \n" + + " \"backend_roles\" : [ ],\n" + + " \"hosts\" : [ ],\n" + + " \"users\" : [$usersStr]\n" + + "}" + request.setJsonEntity(entity) + client().performRequest(request) + } + + fun addPatchUserRolesMapping(role: String, users: Array) { + val request = Request("PATCH", "/_plugins/_security/api/rolesmapping/$role") + val usersStr = users.joinToString { it -> "\"$it\"" } + + val entity = "[{\n" + + " \"op\" : \"add\",\n" + + " \"path\" : \"users\",\n" + + " \"value\" : [$usersStr]\n" + + "}]" + + request.setJsonEntity(entity) + client().performRequest(request) + } + + fun removePatchUserRolesMapping(role: String, users: Array) { + val request = Request("PATCH", "/_plugins/_security/api/rolesmapping/$role") + val usersStr = users.joinToString { it -> "\"$it\"" } + + val entity = "[{\n" + + " \"op\" : \"remove\",\n" + + " \"path\" : \"users\",\n" + + " \"value\" : [$usersStr]\n" + + "}]" + + request.setJsonEntity(entity) + client().performRequest(request) + } + + fun deleteUserRolesMapping(role: String) { + val request = Request("DELETE", "/_plugins/_security/api/rolesmapping/$role") + client().performRequest(request) + } + + fun createCustomRole(name: String, clusterPermissions: String?) { + val request = Request("PUT", "/_plugins/_security/api/roles/$name") + val entity = "{\n" + + "\"cluster_permissions\": [\n" + + "\"$clusterPermissions\"\n" + + "],\n" + + "\"tenant_permissions\": []\n" + + "}" + request.setJsonEntity(entity) + client().performRequest(request) + } + + fun deleteCustomRole(name: String) { + val request = Request("DELETE", "/_plugins/_security/api/roles/$name") + client().performRequest(request) + } + + fun createUserWithRoles(user: String, role: String, backendRole: String) { + createUser(user, user, arrayOf(backendRole)) + addPatchUserRolesMapping(role, arrayOf(user)) + } + + fun deleteUserWithRoles(user: String, role: String) { + removePatchUserRolesMapping(role, arrayOf(user)) + deleteUser(user) + } + + fun createUserWithCustomRole( + user: String, + role: String, + backendRole: String, + clusterPermissions: String? + ) { + createUser(user, user, arrayOf(backendRole)) + createCustomRole(role, clusterPermissions) + createUserRolesMapping(role, arrayOf(user)) + } + + fun deleteUserWithCustomRole( + user: String, + role: String + ) { + deleteUserRolesMapping(role) + deleteCustomRole(role) + deleteUser(user) + } + + fun createConfig( + nameSubstring: String = "", + configType: ConfigType = ConfigType.SLACK, + isEnabled: Boolean = true, + smtpAccountId: String = "", + emailGroupId: Set = setOf(), + client: RestClient = client() + ): String { + val createRequestJsonString = getCreateNotificationRequestJsonString( + nameSubstring, + configType, + isEnabled, + smtpAccountId, + emailGroupId + ) + val createResponse = executeRequest( + RestRequest.Method.POST.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs", + createRequestJsonString, + RestStatus.OK.status, + client + ) + val configId = createResponse.get("config_id").asString + Assert.assertNotNull(configId) + Thread.sleep(100) + return configId + } + @After open fun wipeAllSettings() { wipeAllClusterSettings() } @Throws(IOException::class) - protected fun updateClusterSettings(setting: ClusterSetting): JsonObject { + protected fun updateClusterSettings(setting: ClusterSetting, client: RestClient = client()): JsonObject { val request = Request("PUT", "/_cluster/settings") val persistentSetting = "{\"${setting.type}\": {\"${setting.name}\": ${setting.value}}}" request.setJsonEntity(persistentSetting) val restOptionsBuilder = RequestOptions.DEFAULT.toBuilder() restOptionsBuilder.addHeader("Content-Type", "application/json") request.setOptions(restOptionsBuilder) - return executeRequest(request) + return executeRequest(request, client = client) } @Throws(IOException::class) diff --git a/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SecurityNotificationIT.kt b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SecurityNotificationIT.kt new file mode 100644 index 00000000..47ab2b5e --- /dev/null +++ b/notifications/notifications/src/test/kotlin/org/opensearch/integtest/SecurityNotificationIT.kt @@ -0,0 +1,545 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.integtest + +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.BeforeClass +import org.opensearch.client.RestClient +import org.opensearch.commons.notifications.model.ConfigType +import org.opensearch.commons.notifications.model.NotificationConfig +import org.opensearch.commons.notifications.model.Slack +import org.opensearch.commons.rest.SecureRestClientBuilder +import org.opensearch.notifications.NotificationPlugin +import org.opensearch.notifications.verifyChannelIdEquals +import org.opensearch.notifications.verifySingleConfigEquals +import org.opensearch.rest.RestRequest +import org.opensearch.rest.RestStatus + +class SecurityNotificationIT : PluginRestTestCase() { + + companion object { + @BeforeClass + @JvmStatic fun setup() { + // things to execute once and keep around for the class + org.junit.Assume.assumeTrue(System.getProperty("https", "false")!!.toBoolean()) + } + } + + val user = "integTestUser" + var userClient: RestClient? = null + + @Before + fun create() { + + if (userClient == null) { + createUser(user, user, arrayOf()) + userClient = SecureRestClientBuilder(clusterHosts.toTypedArray(), isHttps(), user, user).setSocketTimeout(60000).build() + } + } + + @After + fun cleanup() { + + userClient?.close() + } + + fun `test Create slack notification config with user that has create Notification permission`() { + createUserWithCustomRole(user, NOTIFICATION_CREATE_CONFIG_ACCESS, "", ROLE_TO_PERMISSION_MAPPING[NOTIFICATION_CREATE_CONFIG_ACCESS]) + + // Create sample config request reference + val sampleSlack = Slack("https://domain.com/sample_slack_url#1234567890") + val referenceObject = NotificationConfig( + "this is a sample config name", + "this is a sample config description", + ConfigType.SLACK, + isEnabled = true, + configData = sampleSlack + ) + + // Create slack notification config + val createRequestJsonString = """ + { + "config":{ + "name":"${referenceObject.name}", + "description":"${referenceObject.description}", + "config_type":"slack", + "is_enabled":${referenceObject.isEnabled}, + "slack":{"url":"${(referenceObject.configData as Slack).url}"} + } + } + """.trimIndent() + try { + val createResponse = executeRequest( + RestRequest.Method.POST.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs", + createRequestJsonString, + RestStatus.OK.status, + userClient!! + ) + val configId = createResponse.get("config_id").asString + Assert.assertNotNull(configId) + Thread.sleep(1000) + + // Get Slack notification config + val getConfigResponse = executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.OK.status + ) + verifySingleConfigEquals(configId, referenceObject, getConfigResponse) + } finally { + deleteUserWithCustomRole(user, NOTIFICATION_CREATE_CONFIG_ACCESS) + } + } + + fun `test Create slack notification config without create Notification permission`() { + createUserWithCustomRole(user, NOTIFICATION_NO_ACCESS_ROLE, "", ROLE_TO_PERMISSION_MAPPING[NOTIFICATION_NO_ACCESS_ROLE]) + + // Create sample config request reference + val sampleSlack = Slack("https://domain.com/sample_slack_url#1234567890") + val referenceObject = NotificationConfig( + "this is a sample config name", + "this is a sample config description", + ConfigType.SLACK, + isEnabled = true, + configData = sampleSlack + ) + + // Create slack notification config + val createRequestJsonString = """ + { + "config":{ + "name":"${referenceObject.name}", + "description":"${referenceObject.description}", + "config_type":"slack", + "is_enabled":${referenceObject.isEnabled}, + "slack":{"url":"${(referenceObject.configData as Slack).url}"} + } + } + """.trimIndent() + + executeRequest( + RestRequest.Method.POST.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs", + createRequestJsonString, + RestStatus.FORBIDDEN.status, + userClient!! + ) + deleteUserWithCustomRole(user, NOTIFICATION_NO_ACCESS_ROLE) + } + + fun `test update slack notification config with user that has create Notification permission`() { + createUserWithCustomRole(user, NOTIFICATION_UPDATE_CONFIG_ACCESS, "", ROLE_TO_PERMISSION_MAPPING[NOTIFICATION_UPDATE_CONFIG_ACCESS]) + + // Create sample config request reference + val sampleSlack = Slack("https://domain.com/sample_slack_url#1234567890") + val referenceObject = NotificationConfig( + "this is a sample config name", + "this is a sample config description", + ConfigType.SLACK, + isEnabled = true, + configData = sampleSlack + ) + + // Create slack notification config + val createRequestJsonString = """ + { + "config":{ + "name":"${referenceObject.name}", + "description":"${referenceObject.description}", + "config_type":"slack", + "is_enabled":${referenceObject.isEnabled}, + "slack":{"url":"${(referenceObject.configData as Slack).url}"} + } + } + """.trimIndent() + val createResponse = executeRequest( + RestRequest.Method.POST.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs", + createRequestJsonString, + RestStatus.OK.status + ) + val configId = createResponse.get("config_id").asString + Assert.assertNotNull(configId) + Thread.sleep(1000) + + // Get Slack notification config + var getConfigResponse = executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.OK.status + ) + verifySingleConfigEquals(configId, referenceObject, getConfigResponse) + + val referenceObjectUpdate = NotificationConfig( + "this is a sample config name updated", + "this is a sample config description updated", + ConfigType.SLACK, + isEnabled = true, + configData = sampleSlack + ) + val updateRequestJsonString = """ + { + "config":{ + "name":"${referenceObjectUpdate.name}", + "description":"${referenceObjectUpdate.description}", + "config_type":"slack", + "is_enabled":${referenceObjectUpdate.isEnabled}, + "slack":{"url":"${(referenceObjectUpdate.configData as Slack).url}"} + } + } + """.trimIndent() + executeRequest( + RestRequest.Method.PUT.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + updateRequestJsonString, + RestStatus.OK.status, + userClient!! + ) + Thread.sleep(1000) + + // Get Slack notification config + getConfigResponse = executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.OK.status + ) + verifySingleConfigEquals(configId, referenceObjectUpdate, getConfigResponse) + + deleteUserWithCustomRole(user, NOTIFICATION_UPDATE_CONFIG_ACCESS) + } + + fun `test update slack notification config without create Notification permission`() { + createUserWithCustomRole(user, NOTIFICATION_NO_ACCESS_ROLE, "", ROLE_TO_PERMISSION_MAPPING[NOTIFICATION_NO_ACCESS_ROLE]) + + // Create sample config request reference + val sampleSlack = Slack("https://domain.com/sample_slack_url#1234567890") + val referenceObject = NotificationConfig( + "this is a sample config name", + "this is a sample config description", + ConfigType.SLACK, + isEnabled = true, + configData = sampleSlack + ) + + // Create slack notification config + val createRequestJsonString = """ + { + "config":{ + "name":"${referenceObject.name}", + "description":"${referenceObject.description}", + "config_type":"slack", + "is_enabled":${referenceObject.isEnabled}, + "slack":{"url":"${(referenceObject.configData as Slack).url}"} + } + } + """.trimIndent() + + executeRequest( + RestRequest.Method.POST.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs", + createRequestJsonString, + RestStatus.FORBIDDEN.status, + userClient!! + ) + deleteUserWithCustomRole(user, NOTIFICATION_NO_ACCESS_ROLE) + } + + fun `test get slack notification config with user that has get Notification permission`() { + createUserWithCustomRole(user, NOTIFICATION_GET_CONFIG_ACCESS, "", ROLE_TO_PERMISSION_MAPPING[NOTIFICATION_GET_CONFIG_ACCESS]) + + // Create sample config request reference + val sampleSlack = Slack("https://domain.com/sample_slack_url#1234567890") + val referenceObject = NotificationConfig( + "this is a sample config name", + "this is a sample config description", + ConfigType.SLACK, + isEnabled = true, + configData = sampleSlack + ) + + // Create slack notification config + val createRequestJsonString = """ + { + "config":{ + "name":"${referenceObject.name}", + "description":"${referenceObject.description}", + "config_type":"slack", + "is_enabled":${referenceObject.isEnabled}, + "slack":{"url":"${(referenceObject.configData as Slack).url}"} + } + } + """.trimIndent() + val createResponse = executeRequest( + RestRequest.Method.POST.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs", + createRequestJsonString, + RestStatus.OK.status + ) + val configId = createResponse.get("config_id").asString + Assert.assertNotNull(configId) + Thread.sleep(1000) + + // Get Slack notification config + val getConfigResponse = executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.OK.status, + userClient!! + ) + verifySingleConfigEquals(configId, referenceObject, getConfigResponse) + deleteUserWithCustomRole(user, NOTIFICATION_GET_CONFIG_ACCESS) + } + + fun `test get slack notification config without get Notification permission`() { + createUserWithCustomRole(user, NOTIFICATION_NO_ACCESS_ROLE, "", ROLE_TO_PERMISSION_MAPPING[NOTIFICATION_NO_ACCESS_ROLE]) + + // Get Slack notification config + + executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/randomConfig", + "", + RestStatus.FORBIDDEN.status, + userClient!! + ) + deleteUserWithCustomRole(user, NOTIFICATION_NO_ACCESS_ROLE) + } + + fun `test delete slack notification config with user that has get Notification permission`() { + createUserWithCustomRole(user, NOTIFICATION_DELETE_CONFIG_ACCESS, "", ROLE_TO_PERMISSION_MAPPING[NOTIFICATION_DELETE_CONFIG_ACCESS]) + + // Create sample config request reference + val sampleSlack = Slack("https://domain.com/sample_slack_url#1234567890") + val referenceObject = NotificationConfig( + "this is a sample config name", + "this is a sample config description", + ConfigType.SLACK, + isEnabled = true, + configData = sampleSlack + ) + + // Create slack notification config + val createRequestJsonString = """ + { + "config":{ + "name":"${referenceObject.name}", + "description":"${referenceObject.description}", + "config_type":"slack", + "is_enabled":${referenceObject.isEnabled}, + "slack":{"url":"${(referenceObject.configData as Slack).url}"} + } + } + """.trimIndent() + val createResponse = executeRequest( + RestRequest.Method.POST.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs", + createRequestJsonString, + RestStatus.OK.status + ) + val configId = createResponse.get("config_id").asString + Assert.assertNotNull(configId) + Thread.sleep(1000) + + // Delete Slack notification config + executeRequest( + RestRequest.Method.DELETE.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.OK.status, + userClient!! + ) + + // Should not be able to find config + executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/$configId", + "", + RestStatus.NOT_FOUND.status + ) + + deleteUserWithCustomRole(user, NOTIFICATION_DELETE_CONFIG_ACCESS) + } + + fun `test delete slack notification config without get Notification permission`() { + createUserWithCustomRole(user, NOTIFICATION_NO_ACCESS_ROLE, "", ROLE_TO_PERMISSION_MAPPING[NOTIFICATION_NO_ACCESS_ROLE]) + + // Get Slack notification config + + executeRequest( + RestRequest.Method.DELETE.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs/randomConfig", + "", + RestStatus.FORBIDDEN.status, + userClient!! + ) + deleteUserWithCustomRole(user, NOTIFICATION_NO_ACCESS_ROLE) + } + + fun `test getChannelList should return only channels with get channel permission`() { + createUserWithCustomRole(user, NOTIFICATION_GET_CHANNEL_ACCESS, "", ROLE_TO_PERMISSION_MAPPING[NOTIFICATION_GET_CHANNEL_ACCESS]) + + val slackId = createConfig(configType = ConfigType.SLACK) + val chimeId = createConfig(configType = ConfigType.CHIME) + val webhookId = createConfig(configType = ConfigType.WEBHOOK) + val emailGroupId = createConfig(configType = ConfigType.EMAIL_GROUP) + val smtpAccountId = createConfig(configType = ConfigType.SMTP_ACCOUNT) + val emailId = createConfig( + configType = ConfigType.EMAIL, + smtpAccountId = smtpAccountId, + emailGroupId = setOf(emailGroupId) + ) + Thread.sleep(1000) + + val channelIds = setOf(slackId, chimeId, webhookId, emailId) + val response = executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/channels", + "", + RestStatus.OK.status, + userClient!! + ) + Thread.sleep(100) + verifyChannelIdEquals(channelIds, response, channelIds.size) + + deleteUserWithCustomRole(user, NOTIFICATION_GET_CHANNEL_ACCESS) + } + + fun `test getChannelList fails without get channel permission`() { + createUserWithCustomRole(user, NOTIFICATION_NO_ACCESS_ROLE, "", ROLE_TO_PERMISSION_MAPPING[NOTIFICATION_NO_ACCESS_ROLE]) + + createConfig(configType = ConfigType.SLACK) + Thread.sleep(1000) + + executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/channels", + "", + RestStatus.FORBIDDEN.status, + userClient!! + ) + + deleteUserWithCustomRole(user, NOTIFICATION_NO_ACCESS_ROLE) + } + + fun `test Get plugin features should return non-empty configTypes with get features permission`() { + createUserWithCustomRole(user, NOTIFICATION_GET_PLUGIN_FEATURE_ACCESS, "", ROLE_TO_PERMISSION_MAPPING[NOTIFICATION_GET_PLUGIN_FEATURE_ACCESS]) + + val getResponse = executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/features", + "", + RestStatus.OK.status, + userClient!! + ) + Assert.assertFalse(getResponse.get("allowed_config_type_list").asJsonArray.isEmpty) + val pluginFeatures = getResponse.get("plugin_features").asJsonObject + Assert.assertFalse(pluginFeatures.keySet().isEmpty()) + deleteUserWithCustomRole(user, NOTIFICATION_GET_PLUGIN_FEATURE_ACCESS) + } + + fun `test Get plugin features fails without get features permission`() { + createUserWithCustomRole(user, NOTIFICATION_NO_ACCESS_ROLE, "", ROLE_TO_PERMISSION_MAPPING[NOTIFICATION_NO_ACCESS_ROLE]) + + executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/features", + "", + RestStatus.FORBIDDEN.status, + userClient!! + ) + deleteUserWithCustomRole(user, NOTIFICATION_NO_ACCESS_ROLE) + } + + fun `test send test slack message with send permissions`() { + createUserWithCustomRole(user, NOTIFICATION_TEST_SEND_ACCESS, "", ROLE_TO_PERMISSION_MAPPING[NOTIFICATION_TEST_SEND_ACCESS]) + + // Create webhook notification config + val createRequestJsonString = """ + { + "config":{ + "name":"this is a sample config name", + "description":"this is a sample config description", + "config_type":"slack", + "is_enabled":true, + "slack":{ + "url":"https://hooks.slack.com/services/xxx/xxx" + } + } + } + """.trimIndent() + val createResponse = executeRequest( + RestRequest.Method.POST.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs", + createRequestJsonString, + RestStatus.OK.status + ) + val configId = createResponse.get("config_id").asString + Assert.assertNotNull(configId) + Thread.sleep(1000) + + // send test message + val sendResponse = executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/feature/test/$configId", + "", + RestStatus.INTERNAL_SERVER_ERROR.status, + userClient!! + ) + + // verify failure response is with message + val error = sendResponse.get("error").asJsonObject + Assert.assertNotNull(error.get("reason").asString) + Assert.assertTrue(error.get("reason").asString.contains("\"delivery_status\":{\"status_code\":\"500\"")) + + deleteUserWithCustomRole(user, NOTIFICATION_TEST_SEND_ACCESS) + } + + fun `test send test slack message without send permissions`() { + createUserWithCustomRole(user, NOTIFICATION_NO_ACCESS_ROLE, "", ROLE_TO_PERMISSION_MAPPING[NOTIFICATION_NO_ACCESS_ROLE]) + + // Create webhook notification config + val createRequestJsonString = """ + { + "config":{ + "name":"this is a sample config name", + "description":"this is a sample config description", + "config_type":"slack", + "is_enabled":true, + "slack":{ + "url":"https://hooks.slack.com/services/xxx/xxx" + } + } + } + """.trimIndent() + val createResponse = executeRequest( + RestRequest.Method.POST.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/configs", + createRequestJsonString, + RestStatus.OK.status + ) + val configId = createResponse.get("config_id").asString + Assert.assertNotNull(configId) + Thread.sleep(1000) + + // send test message + executeRequest( + RestRequest.Method.GET.name, + "${NotificationPlugin.PLUGIN_BASE_URI}/feature/test/$configId", + "", + RestStatus.FORBIDDEN.status, + userClient!! + ) + + deleteUserWithCustomRole(user, NOTIFICATION_NO_ACCESS_ROLE) + } +} 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 fd4e80dd..09324734 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 @@ -5,117 +5,14 @@ package org.opensearch.integtest.features -import com.google.gson.JsonObject -import org.junit.Assert import org.opensearch.commons.notifications.model.ConfigType import org.opensearch.integtest.PluginRestTestCase import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI +import org.opensearch.notifications.verifyChannelIdEquals import org.opensearch.rest.RestRequest import org.opensearch.rest.RestStatus -import kotlin.random.Random class GetNotificationChannelListIT : PluginRestTestCase() { - private val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') - - private fun getCreateRequestJsonString( - nameSubstring: String, - configType: ConfigType, - isEnabled: Boolean, - smtpAccountId: String = "", - emailGroupId: Set = setOf() - ): String { - val randomString = (1..20) - .map { Random.nextInt(0, charPool.size) } - .map(charPool::get) - .joinToString("") - val configObjectString = when (configType) { - ConfigType.SLACK -> """ - "slack":{"url":"https://slack.domain.com/sample_slack_url#$randomString"} - """.trimIndent() - ConfigType.CHIME -> """ - "chime":{"url":"https://chime.domain.com/sample_chime_url#$randomString"} - """.trimIndent() - ConfigType.WEBHOOK -> """ - "webhook":{"url":"https://web.domain.com/sample_web_url#$randomString"} - """.trimIndent() - ConfigType.SMTP_ACCOUNT -> """ - "smtp_account":{ - "host":"smtp.domain.com", - "port":"4321", - "method":"ssl", - "from_address":"$randomString@from.com" - } - """.trimIndent() - ConfigType.EMAIL_GROUP -> """ - "email_group":{ - "recipient_list":[ - {"recipient":"$randomString+recipient1@from.com"}, - {"recipient":"$randomString+recipient2@from.com"} - ] - } - """.trimIndent() - ConfigType.EMAIL -> """ - "email":{ - "email_account_id":"$smtpAccountId", - "recipient_list":[{"recipient":"$randomString@from.com"}], - "email_group_id_list":[${emailGroupId.joinToString { "\"$it\"" }}] - } - """.trimIndent() - else -> throw IllegalArgumentException("Unsupported configType=$configType") - } - return """ - { - "config_id":"$randomString", - "config":{ - "name":"$nameSubstring:this is a sample config name $randomString", - "description":"this is a sample config description $randomString", - "config_type":"$configType", - "is_enabled":$isEnabled, - $configObjectString - } - } - """.trimIndent() - } - - private fun createConfig( - nameSubstring: String = "", - configType: ConfigType = ConfigType.SLACK, - isEnabled: Boolean = true, - smtpAccountId: String = "", - emailGroupId: Set = setOf() - ): String { - val createRequestJsonString = getCreateRequestJsonString( - nameSubstring, - configType, - isEnabled, - smtpAccountId, - emailGroupId - ) - val createResponse = executeRequest( - RestRequest.Method.POST.name, - "$PLUGIN_BASE_URI/configs", - createRequestJsonString, - RestStatus.OK.status - ) - val configId = createResponse.get("config_id").asString - Assert.assertNotNull(configId) - Thread.sleep(100) - return configId - } - - private fun verifyChannelIdEquals(idSet: Set, jsonObject: JsonObject, totalHits: Int = -1) { - if (totalHits >= 0) { - Assert.assertEquals(totalHits, jsonObject.get("total_hits").asInt) - } - val items = jsonObject.get("channel_list").asJsonArray - Assert.assertEquals(idSet.size, items.size()) - items.forEach { - val item = it.asJsonObject - val configId = item.get("config_id").asString - Assert.assertNotNull(configId) - Assert.assertTrue(idSet.contains(configId)) - } - } fun `test POST channel list should result in error`() { executeRequest( 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 2b38314e..e7f01d1f 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 @@ -12,7 +12,6 @@ import org.opensearch.integtest.PluginRestTestCase import org.opensearch.notifications.NotificationPlugin.Companion.PLUGIN_BASE_URI import org.opensearch.rest.RestRequest import org.opensearch.rest.RestStatus -import org.springframework.integration.test.mail.TestMailServer internal class SendTestMessageRestHandlerIT : PluginRestTestCase() { @Suppress("EmptyFunctionBlock") @@ -211,83 +210,86 @@ internal class SendTestMessageRestHandlerIT : PluginRestTestCase() { Assert.assertNotNull(error.get("reason").asString) } - @Suppress("EmptyFunctionBlock") - fun `test send test smtp email message for localhost successfully`() { - if (isLocalHost()) { - val smtpPort = 10255 - val smtpServer = TestMailServer.smtp(smtpPort) - - val sampleSmtpAccount = SmtpAccount( - "localhost", - smtpPort, - MethodType.NONE, - "szhongna@localhost.com" - ) - // Create smtp account notification config - val smtpAccountCreateRequestJsonString = """ - { - "config":{ - "name":"this is a sample smtp", - "description":"this is a sample smtp description", - "config_type":"smtp_account", - "is_enabled":true, - "smtp_account":{ - "host":"${sampleSmtpAccount.host}", - "port":"${sampleSmtpAccount.port}", - "method":"${sampleSmtpAccount.method}", - "from_address":"${sampleSmtpAccount.fromAddress}" - } - } - } - """.trimIndent() - val createResponse = executeRequest( - RestRequest.Method.POST.name, - "$PLUGIN_BASE_URI/configs", - smtpAccountCreateRequestJsonString, - RestStatus.OK.status - ) - val smtpAccountConfigId = createResponse.get("config_id").asString - Assert.assertNotNull(smtpAccountConfigId) - Thread.sleep(1000) - - val emailCreateRequestJsonString = """ - { - "config":{ - "name":"email config name", - "description":"email description", - "config_type":"email", - "is_enabled":true, - "email":{ - "email_account_id":"$smtpAccountConfigId", - "recipient_list":[ - {"recipient":"chloe@localhost.com"} - ], - "email_group_id_list":[] - } - } - } - """.trimIndent() - - val emailCreateResponse = executeRequest( - RestRequest.Method.POST.name, - "$PLUGIN_BASE_URI/configs", - emailCreateRequestJsonString, - RestStatus.OK.status - ) - val emailConfigId = emailCreateResponse.get("config_id").asString - Assert.assertNotNull(emailConfigId) - Thread.sleep(1000) - - // send test message - val sendResponse = executeRequest( - RestRequest.Method.GET.name, - "$PLUGIN_BASE_URI/feature/test/$emailConfigId", - "", - RestStatus.OK.status - ) - - smtpServer.stop() - smtpServer.resetServer() - } - } + /** + * TODO: Needs to be able to detect if running against a docker localhost as this test would fail + */ +// @Suppress("EmptyFunctionBlock") +// fun `test send test smtp email message for localhost successfully`() { +// if (isLocalHost()) { +// val smtpPort = 10255 +// val smtpServer = TestMailServer.smtp(smtpPort) +// +// val sampleSmtpAccount = SmtpAccount( +// "localhost", +// smtpPort, +// MethodType.NONE, +// "szhongna@localhost.com" +// ) +// // Create smtp account notification config +// val smtpAccountCreateRequestJsonString = """ +// { +// "config":{ +// "name":"this is a sample smtp", +// "description":"this is a sample smtp description", +// "config_type":"smtp_account", +// "is_enabled":true, +// "smtp_account":{ +// "host":"${sampleSmtpAccount.host}", +// "port":"${sampleSmtpAccount.port}", +// "method":"${sampleSmtpAccount.method}", +// "from_address":"${sampleSmtpAccount.fromAddress}" +// } +// } +// } +// """.trimIndent() +// val createResponse = executeRequest( +// RestRequest.Method.POST.name, +// "$PLUGIN_BASE_URI/configs", +// smtpAccountCreateRequestJsonString, +// RestStatus.OK.status +// ) +// val smtpAccountConfigId = createResponse.get("config_id").asString +// Assert.assertNotNull(smtpAccountConfigId) +// Thread.sleep(1000) +// +// val emailCreateRequestJsonString = """ +// { +// "config":{ +// "name":"email config name", +// "description":"email description", +// "config_type":"email", +// "is_enabled":true, +// "email":{ +// "email_account_id":"$smtpAccountConfigId", +// "recipient_list":[ +// {"recipient":"chloe@localhost.com"} +// ], +// "email_group_id_list":[] +// } +// } +// } +// """.trimIndent() +// +// val emailCreateResponse = executeRequest( +// RestRequest.Method.POST.name, +// "$PLUGIN_BASE_URI/configs", +// emailCreateRequestJsonString, +// RestStatus.OK.status +// ) +// val emailConfigId = emailCreateResponse.get("config_id").asString +// Assert.assertNotNull(emailConfigId) +// Thread.sleep(1000) +// +// // send test message +// val sendResponse = executeRequest( +// RestRequest.Method.GET.name, +// "$PLUGIN_BASE_URI/feature/test/$emailConfigId", +// "", +// RestStatus.OK.status +// ) +// +// smtpServer.stop() +// smtpServer.resetServer() +// } +// } } 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 63dcc5f0..99b4f2cc 100644 --- a/notifications/notifications/src/test/kotlin/org/opensearch/notifications/ObjectEqualsHelpers.kt +++ b/notifications/notifications/src/test/kotlin/org/opensearch/notifications/ObjectEqualsHelpers.kt @@ -172,3 +172,17 @@ fun verifyOrderedConfigList(idList: List, jsonObject: JsonObject, totalH Assert.assertEquals(idList[it - 1], items[it - 1].asJsonObject.get("config_id").asString) } } + +fun verifyChannelIdEquals(idSet: Set, jsonObject: JsonObject, totalHits: Int = -1) { + if (totalHits >= 0) { + Assert.assertEquals(totalHits, jsonObject.get("total_hits").asInt) + } + val items = jsonObject.get("channel_list").asJsonArray + Assert.assertEquals(idSet.size, items.size()) + items.forEach { + val item = it.asJsonObject + val configId = item.get("config_id").asString + Assert.assertNotNull(configId) + Assert.assertTrue(idSet.contains(configId)) + } +}