55
66package org.opensearch.alerting
77
8+ import kotlinx.coroutines.runBlocking
89import org.junit.Before
10+ import org.mockito.ArgumentCaptor
11+ import org.mockito.ArgumentMatchers.any
912import org.mockito.Mockito
13+ import org.mockito.Mockito.verify
1014import org.opensearch.Version
15+ import org.opensearch.action.bulk.BackoffPolicy
1116import org.opensearch.alerting.alerts.AlertIndices
1217import org.opensearch.alerting.settings.AlertingSettings
1318import org.opensearch.alerting.util.getBucketKeysHash
@@ -16,19 +21,26 @@ import org.opensearch.cluster.service.ClusterService
1621import org.opensearch.common.settings.ClusterSettings
1722import org.opensearch.common.settings.Setting
1823import org.opensearch.common.settings.Settings
24+ import org.opensearch.common.unit.TimeValue
1925import org.opensearch.commons.alerting.model.AggregationResultBucket
2026import org.opensearch.commons.alerting.model.Alert
2127import org.opensearch.commons.alerting.model.BucketLevelTrigger
28+ import org.opensearch.commons.alerting.model.DataSources
2229import org.opensearch.commons.alerting.model.Monitor
2330import org.opensearch.commons.alerting.model.action.AlertCategory
2431import org.opensearch.core.xcontent.NamedXContentRegistry
32+ import org.opensearch.remote.metadata.client.BulkDataObjectRequest
33+ import org.opensearch.remote.metadata.client.BulkDataObjectResponse
2534import org.opensearch.remote.metadata.client.SdkClient
2635import org.opensearch.test.ClusterServiceUtils
2736import org.opensearch.test.OpenSearchTestCase
2837import org.opensearch.threadpool.ThreadPool
2938import org.opensearch.transport.client.Client
3039import java.time.Instant
3140import java.time.temporal.ChronoUnit
41+ import java.util.concurrent.CompletableFuture
42+ import org.mockito.Mockito.`when` as whenever
43+
3244class AlertServiceTests : OpenSearchTestCase () {
3345
3446 private lateinit var client: Client
@@ -39,6 +51,7 @@ class AlertServiceTests : OpenSearchTestCase() {
3951
4052 private lateinit var alertIndices: AlertIndices
4153 private lateinit var alertService: AlertService
54+ private lateinit var sdkClient: SdkClient
4255
4356 @Before
4457 fun setup () {
@@ -47,6 +60,7 @@ class AlertServiceTests : OpenSearchTestCase() {
4760 xContentRegistry = Mockito .mock(NamedXContentRegistry ::class .java)
4861 threadPool = Mockito .mock(ThreadPool ::class .java)
4962 clusterService = Mockito .mock(ClusterService ::class .java)
63+ sdkClient = Mockito .mock(SdkClient ::class .java)
5064 settings = Settings .builder().build()
5165 val settingSet = hashSetOf<Setting <* >>()
5266 settingSet.addAll(ClusterSettings .BUILT_IN_CLUSTER_SETTINGS )
@@ -67,7 +81,7 @@ class AlertServiceTests : OpenSearchTestCase() {
6781 clusterService = Mockito .spy(testClusterService)
6882
6983 alertIndices = AlertIndices (settings, client, threadPool, clusterService)
70- alertService = AlertService (client, xContentRegistry, alertIndices, Mockito .mock( SdkClient :: class .java) )
84+ alertService = AlertService (client, xContentRegistry, alertIndices, sdkClient )
7185 }
7286
7387 fun `test getting categorized alerts for bucket-level monitor with no current alerts` () {
@@ -215,6 +229,84 @@ class AlertServiceTests : OpenSearchTestCase() {
215229 assertAlertsExistForBucketKeys(emptyList(), completedAlerts)
216230 }
217231
232+ fun `test saveAlerts COMPLETED state with multiTenancyEnabled puts to alertsIndex without delete` () {
233+ val multiTenantAlertService = AlertService (client, xContentRegistry, alertIndices, sdkClient, multiTenancyEnabled = true )
234+
235+ val trigger = randomBucketLevelTrigger()
236+ val monitor = randomBucketLevelMonitor(triggers = listOf (trigger))
237+ val alert = Alert (
238+ monitor, trigger, Instant .now().truncatedTo(ChronoUnit .MILLIS ), null ,
239+ actionExecutionResults = listOf (randomActionExecutionResult())
240+ ).copy(state = Alert .State .COMPLETED , endTime = Instant .now())
241+
242+ val bulkResponse = Mockito .mock(BulkDataObjectResponse ::class .java)
243+ whenever(bulkResponse.responses).thenReturn(emptyList())
244+ whenever(sdkClient.bulkDataObjectAsync(any(BulkDataObjectRequest ::class .java)))
245+ .thenReturn(CompletableFuture .completedFuture(bulkResponse))
246+
247+ runBlocking {
248+ multiTenantAlertService.saveAlerts(
249+ DataSources (),
250+ listOf (alert),
251+ BackoffPolicy .constantBackoff(TimeValue .timeValueMillis(10 ), 1 ),
252+ routingId = monitor.id
253+ )
254+ }
255+
256+ val captor = ArgumentCaptor .forClass(BulkDataObjectRequest ::class .java)
257+ verify(sdkClient).bulkDataObjectAsync(captor.capture())
258+ val capturedRequest = captor.value
259+ // Should have only put requests (no delete requests) when multiTenancyEnabled
260+ assertTrue(" Expected put requests in bulk" , capturedRequest.requests().isNotEmpty())
261+ assertTrue(
262+ " Expected PutDataObjectRequest targeting alertsIndex" ,
263+ capturedRequest.requests().any { it is org.opensearch.remote.metadata.client.PutDataObjectRequest }
264+ )
265+ assertFalse(
266+ " Expected no DeleteDataObjectRequest when multiTenancyEnabled" ,
267+ capturedRequest.requests().any { it is org.opensearch.remote.metadata.client.DeleteDataObjectRequest }
268+ )
269+ }
270+
271+ fun `test saveAlerts COMPLETED state without multiTenancyEnabled creates delete and history put` () {
272+ val nonMultiTenantAlertService = AlertService (client, xContentRegistry, alertIndices, sdkClient, multiTenancyEnabled = false )
273+
274+ val trigger = randomBucketLevelTrigger()
275+ val monitor = randomBucketLevelMonitor(triggers = listOf (trigger))
276+ val alert = Alert (
277+ monitor, trigger, Instant .now().truncatedTo(ChronoUnit .MILLIS ), null ,
278+ actionExecutionResults = listOf (randomActionExecutionResult())
279+ ).copy(state = Alert .State .COMPLETED , endTime = Instant .now())
280+
281+ val bulkResponse = Mockito .mock(BulkDataObjectResponse ::class .java)
282+ whenever(bulkResponse.responses).thenReturn(emptyList())
283+ whenever(sdkClient.bulkDataObjectAsync(any(BulkDataObjectRequest ::class .java)))
284+ .thenReturn(CompletableFuture .completedFuture(bulkResponse))
285+
286+ runBlocking {
287+ nonMultiTenantAlertService.saveAlerts(
288+ DataSources (),
289+ listOf (alert),
290+ BackoffPolicy .constantBackoff(TimeValue .timeValueMillis(10 ), 1 ),
291+ routingId = monitor.id
292+ )
293+ }
294+
295+ val captor = ArgumentCaptor .forClass(BulkDataObjectRequest ::class .java)
296+ verify(sdkClient).bulkDataObjectAsync(captor.capture())
297+ val capturedRequest = captor.value
298+ // Should have a delete request for the alertsIndex
299+ assertTrue(
300+ " Expected DeleteDataObjectRequest when multiTenancy disabled" ,
301+ capturedRequest.requests().any { it is org.opensearch.remote.metadata.client.DeleteDataObjectRequest }
302+ )
303+ // Should have a put request for alertsHistoryIndex (history enabled by default)
304+ assertTrue(
305+ " Expected PutDataObjectRequest for history index" ,
306+ capturedRequest.requests().any { it is org.opensearch.remote.metadata.client.PutDataObjectRequest }
307+ )
308+ }
309+
218310 private fun createCurrentAlertsFromBucketKeys (
219311 monitor : Monitor ,
220312 trigger : BucketLevelTrigger ,
0 commit comments