From 4426f3401a1bad9ab72ab0f23ea3e55ed60c1dbb Mon Sep 17 00:00:00 2001 From: Kaituo Li Date: Wed, 12 Jun 2024 15:09:53 -0700 Subject: [PATCH] Add Alias Permission in Security Tests The SecureADRestIT.testCreateAnomalyDetectorWithCustomResultIndex test failed because the cat user did not have the necessary permissions to create an alias, as the current custom index name is an alias. This PR addresses this issue by adding the required alias permission. Additionally, this PR includes other tests to improve test coverage. Testing: * Verified that all security tests pass. Signed-off-by: Kaituo Li --- .../ad/AnomalyDetectorJobRunnerTests.java | 4 +- .../ad/AnomalyDetectorProfileRunnerTests.java | 4 +- .../ad/AnomalyDetectorRestTestCase.java | 3 +- .../ad/EntityProfileRunnerTests.java | 2 +- .../ad/MultiEntityProfileRunnerTests.java | 2 +- .../ad/model/AnomalyDetectorJobTests.java | 4 +- .../opensearch/ad/rest/SecureADRestIT.java | 6 +- .../GetAnomalyDetectorResponseTests.java | 2 +- ...etAnomalyDetectorTransportActionTests.java | 4 +- .../SingleStreamProfileRunnerTests.java | 372 ++++++++++++++++++ .../metrics/CardinalityProfileTests.java | 2 +- .../timeseries/NodeStateManagerTests.java | 2 +- .../opensearch/timeseries/TestHelpers.java | 79 +++- 13 files changed, 457 insertions(+), 29 deletions(-) create mode 100644 src/test/java/org/opensearch/forecast/SingleStreamProfileRunnerTests.java diff --git a/src/test/java/org/opensearch/ad/AnomalyDetectorJobRunnerTests.java b/src/test/java/org/opensearch/ad/AnomalyDetectorJobRunnerTests.java index 0db8b9ef5..77ed1226e 100644 --- a/src/test/java/org/opensearch/ad/AnomalyDetectorJobRunnerTests.java +++ b/src/test/java/org/opensearch/ad/AnomalyDetectorJobRunnerTests.java @@ -221,7 +221,7 @@ public void setup() throws Exception { ActionListener listener = (ActionListener) args[1]; if (request.index().equals(CommonName.JOB_INDEX)) { - Job job = TestHelpers.randomAnomalyDetectorJob(true); + Job job = TestHelpers.randomJob(true); listener.onResponse(TestHelpers.createGetResponse(job, randomAlphaOfLength(5), CommonName.JOB_INDEX)); } return null; @@ -788,7 +788,7 @@ public void testMarkResultIndexQueried() throws IOException { doAnswer(invocation -> { ActionListener> listener = invocation.getArgument(1); - listener.onResponse(Optional.of(TestHelpers.randomAnomalyDetectorJob(true, Instant.ofEpochMilli(1602401500000L), null))); + listener.onResponse(Optional.of(TestHelpers.randomJob(true, Instant.ofEpochMilli(1602401500000L), null))); return null; }).when(nodeStateManager).getJob(any(String.class), any(ActionListener.class)); diff --git a/src/test/java/org/opensearch/ad/AnomalyDetectorProfileRunnerTests.java b/src/test/java/org/opensearch/ad/AnomalyDetectorProfileRunnerTests.java index 4036e3665..23229f3b5 100644 --- a/src/test/java/org/opensearch/ad/AnomalyDetectorProfileRunnerTests.java +++ b/src/test/java/org/opensearch/ad/AnomalyDetectorProfileRunnerTests.java @@ -147,11 +147,11 @@ private void setUpClientGet( listener.onFailure(new IndexNotFoundException(CommonName.JOB_INDEX)); break; case DISABLED: - job = TestHelpers.randomAnomalyDetectorJob(false, jobEnabledTime, null); + job = TestHelpers.randomJob(false, jobEnabledTime, null); listener.onResponse(TestHelpers.createGetResponse(job, detector.getId(), CommonName.JOB_INDEX)); break; case ENABLED: - job = TestHelpers.randomAnomalyDetectorJob(true, jobEnabledTime, null); + job = TestHelpers.randomJob(true, jobEnabledTime, null); listener.onResponse(TestHelpers.createGetResponse(job, detector.getId(), CommonName.JOB_INDEX)); break; default: diff --git a/src/test/java/org/opensearch/ad/AnomalyDetectorRestTestCase.java b/src/test/java/org/opensearch/ad/AnomalyDetectorRestTestCase.java index 16d35243d..ef2047466 100644 --- a/src/test/java/org/opensearch/ad/AnomalyDetectorRestTestCase.java +++ b/src/test/java/org/opensearch/ad/AnomalyDetectorRestTestCase.java @@ -485,7 +485,8 @@ public Response createIndexRole(String role, String index) throws IOException { + "\"masked_fields\": [],\n" + "\"allowed_actions\": [\n" + "\"crud\",\n" - + "\"indices:admin/create\"\n" + + "\"indices:admin/create\",\n" + + "\"indices:admin/aliases\"\n" + "]\n" + "}\n" + "],\n" diff --git a/src/test/java/org/opensearch/ad/EntityProfileRunnerTests.java b/src/test/java/org/opensearch/ad/EntityProfileRunnerTests.java index 0ec38eb9a..62ba6a35e 100644 --- a/src/test/java/org/opensearch/ad/EntityProfileRunnerTests.java +++ b/src/test/java/org/opensearch/ad/EntityProfileRunnerTests.java @@ -126,7 +126,7 @@ public void setUp() throws Exception { categoryField = "a"; detector = TestHelpers.randomAnomalyDetectorUsingCategoryFields(detectorId, Arrays.asList(categoryField)); - job = TestHelpers.randomAnomalyDetectorJob(true); + job = TestHelpers.randomJob(true); requiredSamples = 128; client = mock(Client.class); diff --git a/src/test/java/org/opensearch/ad/MultiEntityProfileRunnerTests.java b/src/test/java/org/opensearch/ad/MultiEntityProfileRunnerTests.java index ecf2a91cb..a6d2228f2 100644 --- a/src/test/java/org/opensearch/ad/MultiEntityProfileRunnerTests.java +++ b/src/test/java/org/opensearch/ad/MultiEntityProfileRunnerTests.java @@ -129,7 +129,7 @@ public void setUp() throws Exception { detectorId = "A69pa3UBHuCbh-emo9oR"; detector = TestHelpers.randomAnomalyDetectorUsingCategoryFields(detectorId, Arrays.asList("a")); result = new DetectorInternalState.Builder().lastUpdateTime(Instant.now()); - job = TestHelpers.randomAnomalyDetectorJob(true); + job = TestHelpers.randomJob(true); adTaskManager = mock(ADTaskManager.class); transportService = mock(TransportService.class); doAnswer(invocation -> { diff --git a/src/test/java/org/opensearch/ad/model/AnomalyDetectorJobTests.java b/src/test/java/org/opensearch/ad/model/AnomalyDetectorJobTests.java index df506b010..0996a8d3c 100644 --- a/src/test/java/org/opensearch/ad/model/AnomalyDetectorJobTests.java +++ b/src/test/java/org/opensearch/ad/model/AnomalyDetectorJobTests.java @@ -39,7 +39,7 @@ protected NamedWriteableRegistry writableRegistry() { } public void testParseAnomalyDetectorJob() throws IOException { - Job anomalyDetectorJob = TestHelpers.randomAnomalyDetectorJob(); + Job anomalyDetectorJob = TestHelpers.randomJob(); String anomalyDetectorJobString = TestHelpers .xContentBuilderToString(anomalyDetectorJob.toXContent(TestHelpers.builder(), ToXContent.EMPTY_PARAMS)); anomalyDetectorJobString = anomalyDetectorJobString @@ -50,7 +50,7 @@ public void testParseAnomalyDetectorJob() throws IOException { } public void testSerialization() throws IOException { - Job anomalyDetectorJob = TestHelpers.randomAnomalyDetectorJob(); + Job anomalyDetectorJob = TestHelpers.randomJob(); BytesStreamOutput output = new BytesStreamOutput(); anomalyDetectorJob.writeTo(output); NamedWriteableAwareStreamInput input = new NamedWriteableAwareStreamInput(output.bytes().streamInput(), writableRegistry()); diff --git a/src/test/java/org/opensearch/ad/rest/SecureADRestIT.java b/src/test/java/org/opensearch/ad/rest/SecureADRestIT.java index ebc6df177..97bf428f2 100644 --- a/src/test/java/org/opensearch/ad/rest/SecureADRestIT.java +++ b/src/test/java/org/opensearch/ad/rest/SecureADRestIT.java @@ -409,7 +409,11 @@ public void testCreateAnomalyDetectorWithCustomResultIndex() throws IOException AnomalyDetector detector = cloneDetector(anomalyDetector, resultIndex); // User goat has no permission to create index Exception exception = expectThrows(IOException.class, () -> { createAnomalyDetector(detector, true, goatClient); }); - Assert.assertTrue(exception.getMessage().contains("no permissions for [indices:admin/create]")); + Assert + .assertTrue( + "got " + exception.getMessage(), + exception.getMessage().contains("no permissions for [indices:admin/aliases, indices:admin/create]") + ); // User cat has permission to create index resultIndex = ADCommonName.CUSTOM_RESULT_INDEX_PREFIX + "test2"; diff --git a/src/test/java/org/opensearch/ad/transport/GetAnomalyDetectorResponseTests.java b/src/test/java/org/opensearch/ad/transport/GetAnomalyDetectorResponseTests.java index 236cd2b58..e66dc7cd9 100644 --- a/src/test/java/org/opensearch/ad/transport/GetAnomalyDetectorResponseTests.java +++ b/src/test/java/org/opensearch/ad/transport/GetAnomalyDetectorResponseTests.java @@ -101,7 +101,7 @@ private GetAnomalyDetectorResponse createGetAnomalyDetectorResponse(boolean retu randomLong(), randomLong(), TestHelpers.randomAnomalyDetector(ImmutableList.of(), ImmutableMap.of(), Instant.now().truncatedTo(ChronoUnit.SECONDS)), - TestHelpers.randomAnomalyDetectorJob(), + TestHelpers.randomJob(), returnJob, TestHelpers.randomAdTask(), TestHelpers.randomAdTask(), diff --git a/src/test/java/org/opensearch/ad/transport/GetAnomalyDetectorTransportActionTests.java b/src/test/java/org/opensearch/ad/transport/GetAnomalyDetectorTransportActionTests.java index 1c12aeb08..c41f106cd 100644 --- a/src/test/java/org/opensearch/ad/transport/GetAnomalyDetectorTransportActionTests.java +++ b/src/test/java/org/opensearch/ad/transport/GetAnomalyDetectorTransportActionTests.java @@ -171,7 +171,7 @@ public void testGetAnomalyDetectorRequestNoEntityValue() throws IOException { public void testGetAnomalyDetectorResponse() throws IOException { BytesStreamOutput out = new BytesStreamOutput(); AnomalyDetector detector = TestHelpers.randomAnomalyDetector(ImmutableMap.of("testKey", "testValue"), Instant.now()); - Job adJob = TestHelpers.randomAnomalyDetectorJob(); + Job adJob = TestHelpers.randomJob(); GetAnomalyDetectorResponse response = new GetAnomalyDetectorResponse( 4321, "1234", @@ -205,7 +205,7 @@ public void testGetAnomalyDetectorResponse() throws IOException { public void testGetAnomalyDetectorProfileResponse() throws IOException { BytesStreamOutput out = new BytesStreamOutput(); AnomalyDetector detector = TestHelpers.randomAnomalyDetector(ImmutableMap.of("testKey", "testValue"), Instant.now()); - Job adJob = TestHelpers.randomAnomalyDetectorJob(); + Job adJob = TestHelpers.randomJob(); InitProgressProfile initProgress = new InitProgressProfile("99%", 2L, 2); EntityProfile entityProfile = new EntityProfile.Builder().initProgress(initProgress).build(); GetAnomalyDetectorResponse response = new GetAnomalyDetectorResponse( diff --git a/src/test/java/org/opensearch/forecast/SingleStreamProfileRunnerTests.java b/src/test/java/org/opensearch/forecast/SingleStreamProfileRunnerTests.java new file mode 100644 index 000000000..abe3d1fcf --- /dev/null +++ b/src/test/java/org/opensearch/forecast/SingleStreamProfileRunnerTests.java @@ -0,0 +1,372 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.forecast; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.opensearch.Version; +import org.opensearch.action.FailedNodeException; +import org.opensearch.action.get.GetRequest; +import org.opensearch.action.get.GetResponse; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.Client; +import org.opensearch.cluster.ClusterName; +import org.opensearch.cluster.node.DiscoveryNode; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.forecast.constant.ForecastCommonName; +import org.opensearch.forecast.indices.ForecastIndex; +import org.opensearch.forecast.model.ForecastResult; +import org.opensearch.forecast.model.ForecastTask; +import org.opensearch.forecast.model.Forecaster; +import org.opensearch.forecast.model.ForecasterProfile; +import org.opensearch.forecast.task.ForecastTaskManager; +import org.opensearch.forecast.transport.ForecastProfileAction; +import org.opensearch.timeseries.AbstractTimeSeriesTest; +import org.opensearch.timeseries.NodeStateManager; +import org.opensearch.timeseries.TestHelpers; +import org.opensearch.timeseries.constant.CommonName; +import org.opensearch.timeseries.model.ConfigProfile; +import org.opensearch.timeseries.model.ConfigState; +import org.opensearch.timeseries.model.Job; +import org.opensearch.timeseries.model.ProfileName; +import org.opensearch.timeseries.transport.ProfileNodeResponse; +import org.opensearch.timeseries.transport.ProfileResponse; +import org.opensearch.timeseries.util.DiscoveryNodeFilterer; +import org.opensearch.timeseries.util.SecurityClientUtil; +import org.opensearch.transport.TransportService; + +public class SingleStreamProfileRunnerTests extends AbstractTimeSeriesTest { + private ForecastProfileRunner runner; + private Client client; + private SecurityClientUtil clientUtil; + private DiscoveryNodeFilterer nodeFilter; + private int requiredSamples; + private Forecaster forecaster; + private String forecasterId; + private Set stateNError; + private String node1; + private String nodeName1; + private DiscoveryNode discoveryNode1; + + private String node2; + private String nodeName2; + private DiscoveryNode discoveryNode2; + + private long modelSize; + private String model1Id; + private String model0Id; + + private Job job; + private TransportService transportService; + private ForecastTaskManager forecastTaskManager; + private ForecastTaskProfileRunner taskProfileRunner; + private ForecastTask task; + + enum InittedEverResultStatus { + INITTED, + NOT_INITTED, + } + + @BeforeClass + public static void setUpBeforeClass() { + setUpThreadPool(SingleStreamProfileRunnerTests.class.getSimpleName()); + } + + @AfterClass + public static void tearDownAfterClass() { + tearDownThreadPool(); + } + + @SuppressWarnings("unchecked") + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + client = mock(Client.class); + taskProfileRunner = mock(ForecastTaskProfileRunner.class); + NodeStateManager nodeStateManager = mock(NodeStateManager.class); + clientUtil = new SecurityClientUtil(nodeStateManager, Settings.EMPTY); + nodeFilter = mock(DiscoveryNodeFilterer.class); + requiredSamples = 128; + + forecasterId = "A69pa3UBHuCbh-emo9oR"; + forecaster = TestHelpers.ForecasterBuilder.newInstance().setConfigId(forecasterId).setCategoryFields(null).build(); + job = TestHelpers.randomJob(true); + forecastTaskManager = mock(ForecastTaskManager.class); + transportService = mock(TransportService.class); + task = TestHelpers.ForecastTaskBuilder.newInstance().build(); + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + Consumer> function = (Consumer>) args[2]; + + function.accept(Optional.of(task)); + return null; + }).when(forecastTaskManager).getAndExecuteOnLatestConfigLevelTask(any(), any(), any(), any(), anyBoolean(), any()); + runner = new ForecastProfileRunner( + client, + clientUtil, + xContentRegistry(), + nodeFilter, + requiredSamples, + transportService, + forecastTaskManager, + taskProfileRunner + ); + + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + GetRequest request = (GetRequest) args[0]; + ActionListener listener = (ActionListener) args[1]; + + String indexName = request.index(); + if (indexName.equals(CommonName.CONFIG_INDEX)) { + listener.onResponse(TestHelpers.createGetResponse(forecaster, forecaster.getId(), CommonName.CONFIG_INDEX)); + } else if (indexName.equals(ForecastIndex.STATE.getIndexName())) { + listener.onResponse(TestHelpers.createGetResponse(task, forecaster.getId(), ForecastIndex.STATE.getIndexName())); + } else if (indexName.equals(CommonName.JOB_INDEX)) { + listener.onResponse(TestHelpers.createGetResponse(job, forecaster.getId(), CommonName.JOB_INDEX)); + } + + return null; + }).when(client).get(any(), any()); + + stateNError = new HashSet(); + stateNError.add(ProfileName.ERROR); + stateNError.add(ProfileName.STATE); + } + + @SuppressWarnings("unchecked") + private void setUpClientExecuteProfileAction(InittedEverResultStatus initted) { + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + ActionListener listener = (ActionListener) args[2]; + + node1 = "node1"; + nodeName1 = "nodename1"; + discoveryNode1 = new DiscoveryNode( + nodeName1, + node1, + new TransportAddress(TransportAddress.META_ADDRESS, 9300), + emptyMap(), + emptySet(), + Version.CURRENT + ); + + node2 = "node2"; + nodeName2 = "nodename2"; + discoveryNode2 = new DiscoveryNode( + nodeName2, + node2, + new TransportAddress(TransportAddress.META_ADDRESS, 9301), + emptyMap(), + emptySet(), + Version.CURRENT + ); + + modelSize = 712480L; + model1Id = "A69pa3UBHuCbh-emo9oR_entity_host1"; + model0Id = "A69pa3UBHuCbh-emo9oR_entity_host0"; + + String clusterName = "test-cluster-name"; + + Map modelSizeMap1 = new HashMap() { + { + put(model1Id, modelSize); + } + }; + + Map modelSizeMap2 = new HashMap() { + { + put(model0Id, modelSize); + } + }; + + // one model in each node; all fully initialized + long updates = requiredSamples - 1; + if (InittedEverResultStatus.INITTED == initted) { + updates = requiredSamples + 1; + } + ProfileNodeResponse profileNodeResponse1 = new ProfileNodeResponse( + discoveryNode1, + modelSizeMap1, + 1L, + updates, + new ArrayList<>(), + modelSizeMap1.size(), + false + ); + ProfileNodeResponse profileNodeResponse2 = new ProfileNodeResponse( + discoveryNode2, + modelSizeMap2, + 1L, + updates, + new ArrayList<>(), + modelSizeMap2.size(), + false + ); + List profileNodeResponses = Arrays.asList(profileNodeResponse1, profileNodeResponse2); + List failures = Collections.emptyList(); + ProfileResponse profileResponse = new ProfileResponse(new ClusterName(clusterName), profileNodeResponses, failures); + + listener.onResponse(profileResponse); + + return null; + }).when(client).execute(any(ForecastProfileAction.class), any(), any()); + + } + + @SuppressWarnings("unchecked") + private void setUpClientSearch(InittedEverResultStatus inittedEverResultStatus) { + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + SearchRequest request = (SearchRequest) args[0]; + ActionListener listener = (ActionListener) args[1]; + + ForecastResult result = null; + if (request.source().query().toString().contains(ForecastResult.VALUE_FIELD)) { + switch (inittedEverResultStatus) { + case INITTED: + result = TestHelpers.ForecastResultBuilder.newInstance().build(); + listener.onResponse(TestHelpers.createSearchResponse(result)); + break; + case NOT_INITTED: + listener.onResponse(TestHelpers.createEmptySearchResponse()); + break; + default: + assertTrue("should not reach here", false); + break; + } + } + + return null; + }).when(client).search(any(), any()); + } + + public void testInit() throws InterruptedException { + setUpClientExecuteProfileAction(InittedEverResultStatus.NOT_INITTED); + setUpClientSearch(InittedEverResultStatus.NOT_INITTED); + + final CountDownLatch inProgressLatch = new CountDownLatch(1); + + ConfigProfile expectedProfile = new ForecasterProfile.Builder().state(ConfigState.INIT).build(); + runner.profile(forecasterId, ActionListener.wrap(response -> { + assertEquals(expectedProfile, response); + inProgressLatch.countDown(); + }, exception -> { + assertTrue("Should not reach here", false); + inProgressLatch.countDown(); + }), stateNError); + assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + } + + public void testRunning() throws InterruptedException { + setUpClientExecuteProfileAction(InittedEverResultStatus.INITTED); + setUpClientSearch(InittedEverResultStatus.INITTED); + + final CountDownLatch inProgressLatch = new CountDownLatch(1); + + ConfigProfile expectedProfile = new ForecasterProfile.Builder().state(ConfigState.RUNNING).build(); + runner.profile(forecasterId, ActionListener.wrap(response -> { + assertEquals(expectedProfile, response); + inProgressLatch.countDown(); + }, exception -> { + assertTrue("Should not reach here", false); + inProgressLatch.countDown(); + }), stateNError); + assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + } + + /** + * Although profile action results indicate not initted, we trust what result index tells us + * @throws InterruptedException if CountDownLatch is interrupted while waiting + */ + public void testResultIndexFinalTruth() throws InterruptedException { + setUpClientExecuteProfileAction(InittedEverResultStatus.NOT_INITTED); + setUpClientSearch(InittedEverResultStatus.INITTED); + + final CountDownLatch inProgressLatch = new CountDownLatch(1); + + ConfigProfile expectedProfile = new ForecasterProfile.Builder().state(ConfigState.RUNNING).build(); + runner.profile(forecasterId, ActionListener.wrap(response -> { + assertEquals(expectedProfile, response); + inProgressLatch.countDown(); + }, exception -> { + assertTrue("Should not reach here", false); + inProgressLatch.countDown(); + }), stateNError); + assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + } + + /** + * Although profile action results indicate not initted, we trust what result index tells us + * @throws InterruptedException if CountDownLatch is interrupted while waiting + * @throws IOException + */ + public void testCustomResultIndexFinalTruth() throws InterruptedException, IOException { + setUpClientExecuteProfileAction(InittedEverResultStatus.NOT_INITTED); + setUpClientSearch(InittedEverResultStatus.INITTED); + + forecaster = TestHelpers.ForecasterBuilder + .newInstance() + .setConfigId(forecasterId) + .setCategoryFields(null) + .setCustomResultIndex(ForecastCommonName.CUSTOM_RESULT_INDEX_PREFIX + "test-index") + .build(); + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + GetRequest request = (GetRequest) args[0]; + ActionListener listener = (ActionListener) args[1]; + + String indexName = request.index(); + if (indexName.equals(CommonName.CONFIG_INDEX)) { + listener.onResponse(TestHelpers.createGetResponse(forecaster, forecaster.getId(), CommonName.CONFIG_INDEX)); + } else if (indexName.equals(ForecastIndex.STATE.getIndexName())) { + listener.onResponse(TestHelpers.createGetResponse(task, forecaster.getId(), ForecastIndex.STATE.getIndexName())); + } else if (indexName.equals(CommonName.JOB_INDEX)) { + listener.onResponse(TestHelpers.createGetResponse(job, forecaster.getId(), CommonName.JOB_INDEX)); + } + + return null; + }).when(client).get(any(), any()); + + final CountDownLatch inProgressLatch = new CountDownLatch(1); + + ConfigProfile expectedProfile = new ForecasterProfile.Builder().state(ConfigState.RUNNING).build(); + runner.profile(forecasterId, ActionListener.wrap(response -> { + assertEquals(expectedProfile, response); + inProgressLatch.countDown(); + }, exception -> { + assertTrue("Should not reach here", false); + inProgressLatch.countDown(); + }), stateNError); + assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + } +} diff --git a/src/test/java/org/opensearch/search/aggregations/metrics/CardinalityProfileTests.java b/src/test/java/org/opensearch/search/aggregations/metrics/CardinalityProfileTests.java index bb37434f3..6dbd8b5e0 100644 --- a/src/test/java/org/opensearch/search/aggregations/metrics/CardinalityProfileTests.java +++ b/src/test/java/org/opensearch/search/aggregations/metrics/CardinalityProfileTests.java @@ -108,7 +108,7 @@ private void setUpMultiEntityClientGet(DetectorStatus detectorStatus, JobStatus Job job = null; switch (jobStatus) { case ENABLED: - job = TestHelpers.randomAnomalyDetectorJob(true); + job = TestHelpers.randomJob(true); listener.onResponse(TestHelpers.createGetResponse(job, detector.getId(), CommonName.JOB_INDEX)); break; default: diff --git a/src/test/java/org/opensearch/timeseries/NodeStateManagerTests.java b/src/test/java/org/opensearch/timeseries/NodeStateManagerTests.java index 4b91e18b5..73acb5cf2 100644 --- a/src/test/java/org/opensearch/timeseries/NodeStateManagerTests.java +++ b/src/test/java/org/opensearch/timeseries/NodeStateManagerTests.java @@ -141,7 +141,7 @@ public void setUp() throws Exception { ); checkpointResponse = mock(GetResponse.class); - jobToCheck = TestHelpers.randomAnomalyDetectorJob(true, Instant.ofEpochMilli(1602401500000L), null); + jobToCheck = TestHelpers.randomJob(true, Instant.ofEpochMilli(1602401500000L), null); } @Override diff --git a/src/test/java/org/opensearch/timeseries/TestHelpers.java b/src/test/java/org/opensearch/timeseries/TestHelpers.java index 7ce60a313..34f8830f1 100644 --- a/src/test/java/org/opensearch/timeseries/TestHelpers.java +++ b/src/test/java/org/opensearch/timeseries/TestHelpers.java @@ -23,6 +23,7 @@ import static org.opensearch.test.OpenSearchTestCase.randomBoolean; import static org.opensearch.test.OpenSearchTestCase.randomDouble; import static org.opensearch.test.OpenSearchTestCase.randomDoubleBetween; +import static org.opensearch.test.OpenSearchTestCase.randomFloat; import static org.opensearch.test.OpenSearchTestCase.randomInt; import static org.opensearch.test.OpenSearchTestCase.randomIntBetween; import static org.opensearch.test.OpenSearchTestCase.randomLong; @@ -115,6 +116,7 @@ import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.forecast.model.ForecastResult; import org.opensearch.forecast.model.ForecastTask; import org.opensearch.forecast.model.Forecaster; import org.opensearch.index.get.GetResult; @@ -713,7 +715,8 @@ public AnomalyDetector build() { user, resultIndex, imputationOption, - randomIntBetween(1, 10000), + // transform decay has to be [0, 1). So we cannot use 1. + randomIntBetween(2, 10000), randomIntBetween(1, TimeSeriesSettings.MAX_SHINGLE_SIZE * 2), // make history intervals at least TimeSeriesSettings.NUM_MIN_SAMPLES. // Otherwise, tests like EntityColdStarterTests.testTwoSegments may fail @@ -912,7 +915,7 @@ public static AnomalyResult randomAnomalyDetectResult() { } public static AnomalyResult randomAnomalyDetectResult(double score) { - return randomAnomalyDetectResult(randomDouble(), null, null); + return randomAnomalyDetectResult(score, null, null); } public static AnomalyResult randomAnomalyDetectResult(String error) { @@ -1044,11 +1047,11 @@ public static AnomalyResult randomHCADAnomalyDetectResult( ); } - public static Job randomAnomalyDetectorJob() { - return randomAnomalyDetectorJob(true); + public static Job randomJob() { + return randomJob(true); } - public static Job randomAnomalyDetectorJob(boolean enabled, Instant enabledTime, Instant disabledTime) { + public static Job randomJob(boolean enabled, Instant enabledTime, Instant disabledTime) { return new Job( randomAlphaOfLength(10), randomIntervalSchedule(), @@ -1064,12 +1067,8 @@ public static Job randomAnomalyDetectorJob(boolean enabled, Instant enabledTime, ); } - public static Job randomAnomalyDetectorJob(boolean enabled) { - return randomAnomalyDetectorJob( - enabled, - Instant.now().truncatedTo(ChronoUnit.SECONDS), - Instant.now().truncatedTo(ChronoUnit.SECONDS) - ); + public static Job randomJob(boolean enabled) { + return randomJob(enabled, Instant.now().truncatedTo(ChronoUnit.SECONDS), Instant.now().truncatedTo(ChronoUnit.SECONDS)); } public static AnomalyDetectorExecutionInput randomAnomalyDetectorExecutionInput() throws IOException { @@ -1935,9 +1934,7 @@ public static class ForecastTaskBuilder { private DateRange dateRange = new DateRange(Instant.ofEpochMilli(123), Instant.ofEpochMilli(456)); - public ForecastTaskBuilder() throws IOException { - forecaster = TestHelpers.randomForecaster(); - } + public ForecastTaskBuilder() throws IOException {} public static ForecastTaskBuilder newInstance() throws IOException { return new ForecastTaskBuilder(); @@ -1991,4 +1988,58 @@ public ForecastTask build() { .build(); } } + + public static class ForecastResultBuilder { + private String forecasterId = randomAlphaOfLength(5); + private String taskId = randomAlphaOfLength(5); + private Double dataQuality = randomDouble(); + private List featureData = ImmutableList.of(randomFeatureData(), randomFeatureData()); + private Instant dataStartTime = Instant.now().truncatedTo(ChronoUnit.SECONDS); + private Instant dataEndTime = Instant.now().truncatedTo(ChronoUnit.SECONDS); + private Instant executionStartTime = Instant.now().truncatedTo(ChronoUnit.SECONDS); + private Instant executionEndTime = Instant.now().truncatedTo(ChronoUnit.SECONDS); + private String error = ""; + private Optional entity = Optional.empty(); + private User user = randomUser(); + private Integer schemaVersion = randomIntBetween(1, 10); + private String featureId = randomAlphaOfLength(5); + private Float forecastValue = randomFloat(); + private Float lowerBound = randomFloat(); + private Float upperBound = randomFloat(); + private Instant forecastDataStartTime = Instant.now().truncatedTo(ChronoUnit.SECONDS); + private Instant forecastDataEndTime = Instant.now().truncatedTo(ChronoUnit.SECONDS); + private Integer horizonIndex = randomIntBetween(1, 10); + + public ForecastResultBuilder() { + + } + + public static ForecastResultBuilder newInstance() { + return new ForecastResultBuilder(); + } + + public ForecastResult build() { + return new ForecastResult( + forecasterId, + taskId, + dataQuality, + featureData, + dataStartTime, + dataEndTime, + executionStartTime, + executionEndTime, + error, + entity, + user, + schemaVersion, + featureId, + forecastValue, + lowerBound, + upperBound, + forecastDataStartTime, + forecastDataEndTime, + horizonIndex + ); + } + } }