diff --git a/src/main/java/org/opensearch/ad/constant/CommonErrorMessages.java b/src/main/java/org/opensearch/ad/constant/CommonErrorMessages.java index bb4f3a35f..1d5b35859 100644 --- a/src/main/java/org/opensearch/ad/constant/CommonErrorMessages.java +++ b/src/main/java/org/opensearch/ad/constant/CommonErrorMessages.java @@ -13,6 +13,7 @@ import static org.opensearch.ad.constant.CommonName.CUSTOM_RESULT_INDEX_PREFIX; import static org.opensearch.ad.model.AnomalyDetector.MAX_RESULT_INDEX_NAME_SIZE; +import static org.opensearch.ad.rest.handler.AbstractAnomalyDetectorActionHandler.MAX_DETECTOR_NAME_SIZE; import java.util.Locale; @@ -51,6 +52,7 @@ public class CommonErrorMessages { public static final String BUG_RESPONSE = "We might have bugs."; public static final String INDEX_NOT_FOUND = "index does not exist"; public static final String NOT_EXISTENT_VALIDATION_TYPE = "The given validation type doesn't exist"; + public static final String UNSUPPORTED_PROFILE_TYPE = "Unsupported profile types"; private static final String TOO_MANY_CATEGORICAL_FIELD_ERR_MSG_FORMAT = "We can have only %d categorical field/s."; @@ -73,8 +75,10 @@ public static String getTooManyCategoricalFieldErr(int limit) { public static String HISTORICAL_ANALYSIS_CANCELLED = "Historical analysis cancelled by user"; public static String HC_DETECTOR_TASK_IS_UPDATING = "HC detector task is updating"; public static String NEGATIVE_TIME_CONFIGURATION = "should be non-negative"; + public static String INVALID_TIME_CONFIGURATION_UNITS = "Time unit %s is not supported"; public static String INVALID_DETECTOR_NAME = "Valid characters for detector name are a-z, A-Z, 0-9, -(hyphen), _(underscore) and .(period)"; + public static String DUPLICATE_FEATURE_AGGREGATION_NAMES = "Detector has duplicate feature aggregation query names: "; public static String FAIL_TO_GET_DETECTOR = "Fail to get detector"; public static String FAIL_TO_GET_DETECTOR_INFO = "Fail to get detector info"; @@ -96,4 +100,7 @@ public static String getTooManyCategoricalFieldErr(int limit) { public static String INVALID_CHAR_IN_RESULT_INDEX_NAME = "Result index name has invalid character. Valid characters are a-z, 0-9, -(hyphen) and _(underscore)"; public static String INVALID_RESULT_INDEX_MAPPING = "Result index mapping is not correct for index: "; + public static String INVALID_DETECTOR_NAME_SIZE = "Name should be shortened. The maximum limit is " + + MAX_DETECTOR_NAME_SIZE + + " characters."; } diff --git a/src/main/java/org/opensearch/ad/model/ADEntityTaskProfile.java b/src/main/java/org/opensearch/ad/model/ADEntityTaskProfile.java index 4233e42c1..2b46372d2 100644 --- a/src/main/java/org/opensearch/ad/model/ADEntityTaskProfile.java +++ b/src/main/java/org/opensearch/ad/model/ADEntityTaskProfile.java @@ -14,6 +14,7 @@ import static org.opensearch.common.xcontent.XContentParserUtils.ensureExpectedToken; import java.io.IOException; +import java.util.Objects; import org.opensearch.common.io.stream.StreamInput; import org.opensearch.common.io.stream.StreamOutput; @@ -269,4 +270,38 @@ public String getAdTaskType() { public void setAdTaskType(String adTaskType) { this.adTaskType = adTaskType; } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ADEntityTaskProfile that = (ADEntityTaskProfile) o; + return Objects.equals(shingleSize, that.shingleSize) + && Objects.equals(rcfTotalUpdates, that.rcfTotalUpdates) + && Objects.equals(thresholdModelTrained, that.thresholdModelTrained) + && Objects.equals(thresholdModelTrainingDataSize, that.thresholdModelTrainingDataSize) + && Objects.equals(modelSizeInBytes, that.modelSizeInBytes) + && Objects.equals(nodeId, that.nodeId) + && Objects.equals(taskId, that.taskId) + && Objects.equals(adTaskType, that.adTaskType) + && Objects.equals(entity, that.entity); + } + + @Override + public int hashCode() { + return Objects + .hash( + shingleSize, + rcfTotalUpdates, + thresholdModelTrained, + thresholdModelTrainingDataSize, + modelSizeInBytes, + nodeId, + entity, + taskId, + adTaskType + ); + } } diff --git a/src/main/java/org/opensearch/ad/model/DetectorProfileName.java b/src/main/java/org/opensearch/ad/model/DetectorProfileName.java index 01ffde83c..2b8f220a3 100644 --- a/src/main/java/org/opensearch/ad/model/DetectorProfileName.java +++ b/src/main/java/org/opensearch/ad/model/DetectorProfileName.java @@ -15,6 +15,7 @@ import java.util.Set; import org.opensearch.ad.Name; +import org.opensearch.ad.constant.CommonErrorMessages; import org.opensearch.ad.constant.CommonName; public enum DetectorProfileName implements Name { @@ -68,7 +69,7 @@ public static DetectorProfileName getName(String name) { case CommonName.AD_TASK: return AD_TASK; default: - throw new IllegalArgumentException("Unsupported profile types"); + throw new IllegalArgumentException(CommonErrorMessages.UNSUPPORTED_PROFILE_TYPE); } } diff --git a/src/main/java/org/opensearch/ad/model/IntervalTimeConfiguration.java b/src/main/java/org/opensearch/ad/model/IntervalTimeConfiguration.java index b9a147bbb..f1fcab5f9 100644 --- a/src/main/java/org/opensearch/ad/model/IntervalTimeConfiguration.java +++ b/src/main/java/org/opensearch/ad/model/IntervalTimeConfiguration.java @@ -46,7 +46,7 @@ public IntervalTimeConfiguration(long interval, ChronoUnit unit) { ); } if (!SUPPORTED_UNITS.contains(unit)) { - throw new IllegalArgumentException(String.format(Locale.ROOT, "Timezone %s is not supported", unit)); + throw new IllegalArgumentException(String.format(Locale.ROOT, CommonErrorMessages.INVALID_TIME_CONFIGURATION_UNITS, unit)); } this.interval = interval; this.unit = unit; diff --git a/src/main/java/org/opensearch/ad/rest/handler/AbstractAnomalyDetectorActionHandler.java b/src/main/java/org/opensearch/ad/rest/handler/AbstractAnomalyDetectorActionHandler.java index 11afab837..0f0995b38 100644 --- a/src/main/java/org/opensearch/ad/rest/handler/AbstractAnomalyDetectorActionHandler.java +++ b/src/main/java/org/opensearch/ad/rest/handler/AbstractAnomalyDetectorActionHandler.java @@ -119,7 +119,7 @@ public abstract class AbstractAnomalyDetectorActionHandler>>( validateFeatureQueriesListener, anomalyDetector.getFeatureAttributes().size(), - String.format(Locale.ROOT, "Validation failed for feature(s) of detector %s", anomalyDetector.getName()), + String.format(Locale.ROOT, VALIDATION_FEATURE_FAILURE, anomalyDetector.getName()), false ); diff --git a/src/main/java/org/opensearch/ad/util/RestHandlerUtils.java b/src/main/java/org/opensearch/ad/util/RestHandlerUtils.java index 3fa28d743..c11918653 100644 --- a/src/main/java/org/opensearch/ad/util/RestHandlerUtils.java +++ b/src/main/java/org/opensearch/ad/util/RestHandlerUtils.java @@ -27,6 +27,7 @@ import org.opensearch.action.search.ShardSearchFailure; import org.opensearch.ad.common.exception.AnomalyDetectionException; import org.opensearch.ad.common.exception.ResourceNotFoundException; +import org.opensearch.ad.constant.CommonErrorMessages; import org.opensearch.ad.model.AnomalyDetector; import org.opensearch.ad.model.Feature; import org.opensearch.common.Strings; @@ -151,7 +152,7 @@ private static String validateFeaturesConfig(List features) { errorMsgBuilder.append(". "); } if (duplicateFeatureAggNames.size() > 0) { - errorMsgBuilder.append("Detector has duplicate feature aggregation query names: "); + errorMsgBuilder.append(CommonErrorMessages.DUPLICATE_FEATURE_AGGREGATION_NAMES); errorMsgBuilder.append(String.join(", ", duplicateFeatureAggNames)); } return errorMsgBuilder.toString(); diff --git a/src/test/java/org/opensearch/action/admin/indices/mapping/get/IndexAnomalyDetectorActionHandlerTests.java b/src/test/java/org/opensearch/action/admin/indices/mapping/get/IndexAnomalyDetectorActionHandlerTests.java index a5bdd5aa3..cbc4dabce 100644 --- a/src/test/java/org/opensearch/action/admin/indices/mapping/get/IndexAnomalyDetectorActionHandlerTests.java +++ b/src/test/java/org/opensearch/action/admin/indices/mapping/get/IndexAnomalyDetectorActionHandlerTests.java @@ -32,6 +32,7 @@ import org.junit.BeforeClass; import org.junit.Ignore; import org.mockito.ArgumentCaptor; +import org.opensearch.OpenSearchStatusException; import org.opensearch.action.ActionListener; import org.opensearch.action.ActionRequest; import org.opensearch.action.ActionResponse; @@ -46,10 +47,12 @@ import org.opensearch.ad.AbstractADTest; import org.opensearch.ad.TestHelpers; import org.opensearch.ad.common.exception.ADValidationException; +import org.opensearch.ad.constant.CommonErrorMessages; import org.opensearch.ad.constant.CommonName; import org.opensearch.ad.feature.SearchFeatureDao; import org.opensearch.ad.indices.AnomalyDetectionIndices; import org.opensearch.ad.model.AnomalyDetector; +import org.opensearch.ad.model.Feature; import org.opensearch.ad.rest.handler.IndexAnomalyDetectorActionHandler; import org.opensearch.ad.task.ADTaskManager; import org.opensearch.ad.transport.IndexAnomalyDetectorResponse; @@ -66,6 +69,8 @@ import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; +import com.google.common.collect.ImmutableList; + /** * * we need to put the test in the same package of GetFieldMappingsResponse @@ -703,4 +708,50 @@ public void testTenMultiEntityDetectorsUpdateExistingMultiEntityAd() throws IOEx assertTrue(value instanceof IllegalStateException); assertTrue(value.getMessage().contains("NodeClient has not been initialized")); } + + @SuppressWarnings("unchecked") + public void testCreateAnomalyDetectorWithDuplicateFeatureAggregationNames() throws IOException { + Feature featureOne = TestHelpers.randomFeature("featureName", "test-1"); + Feature featureTwo = TestHelpers.randomFeature("featureNameTwo", "test-1"); + AnomalyDetector anomalyDetector = TestHelpers.randomAnomalyDetector(ImmutableList.of(featureOne, featureTwo)); + SearchResponse mockResponse = mock(SearchResponse.class); + when(mockResponse.getHits()).thenReturn(TestHelpers.createSearchHits(9)); + doAnswer(invocation -> { + Object[] args = invocation.getArguments(); + assertTrue(String.format("The size of args is %d. Its content is %s", args.length, Arrays.toString(args)), args.length == 2); + assertTrue(args[0] instanceof SearchRequest); + assertTrue(args[1] instanceof ActionListener); + ActionListener listener = (ActionListener) args[1]; + listener.onResponse(mockResponse); + return null; + }).when(clientMock).search(any(SearchRequest.class), any()); + + handler = new IndexAnomalyDetectorActionHandler( + clusterService, + clientMock, + transportService, + channel, + anomalyDetectionIndices, + detectorId, + seqNo, + primaryTerm, + refreshPolicy, + anomalyDetector, + requestTimeout, + maxSingleEntityAnomalyDetectors, + maxMultiEntityAnomalyDetectors, + maxAnomalyFeatures, + method, + xContentRegistry(), + null, + adTaskManager, + searchFeatureDao + ); + ArgumentCaptor response = ArgumentCaptor.forClass(Exception.class); + handler.start(); + verify(channel).onFailure(response.capture()); + Exception value = response.getValue(); + assertTrue(value instanceof OpenSearchStatusException); + assertTrue(value.getMessage().contains(CommonErrorMessages.DUPLICATE_FEATURE_AGGREGATION_NAMES)); + } } diff --git a/src/test/java/org/opensearch/action/admin/indices/mapping/get/ValidateAnomalyDetectorActionHandlerTests.java b/src/test/java/org/opensearch/action/admin/indices/mapping/get/ValidateAnomalyDetectorActionHandlerTests.java index ebb3f4a00..c1d8707cd 100644 --- a/src/test/java/org/opensearch/action/admin/indices/mapping/get/ValidateAnomalyDetectorActionHandlerTests.java +++ b/src/test/java/org/opensearch/action/admin/indices/mapping/get/ValidateAnomalyDetectorActionHandlerTests.java @@ -121,7 +121,7 @@ public void setUp() throws Exception { @SuppressWarnings("unchecked") public void testValidateMoreThanThousandSingleEntityDetectorLimit() throws IOException { SearchResponse mockResponse = mock(SearchResponse.class); - int totalHits = 1001; + int totalHits = maxSingleEntityAnomalyDetectors + 1; when(mockResponse.getHits()).thenReturn(TestHelpers.createSearchHits(totalHits)); doAnswer(invocation -> { Object[] args = invocation.getArguments(); @@ -152,7 +152,6 @@ public void testValidateMoreThanThousandSingleEntityDetectorLimit() throws IOExc searchFeatureDao, ValidationAspect.DETECTOR.getName() ); - handler.start(); ArgumentCaptor response = ArgumentCaptor.forClass(Exception.class); verify(clientMock, never()).execute(eq(GetMappingsAction.INSTANCE), any(), any()); @@ -172,7 +171,7 @@ public void testValidateMoreThanThousandSingleEntityDetectorLimit() throws IOExc public void testValidateMoreThanTenMultiEntityDetectorsLimit() throws IOException { SearchResponse mockResponse = mock(SearchResponse.class); - int totalHits = 11; + int totalHits = maxMultiEntityAnomalyDetectors + 1; when(mockResponse.getHits()).thenReturn(TestHelpers.createSearchHits(totalHits)); @@ -204,7 +203,7 @@ public void testValidateMoreThanTenMultiEntityDetectorsLimit() throws IOExceptio xContentRegistry(), null, searchFeatureDao, - ValidationAspect.DETECTOR.getName() + "" ); handler.start(); diff --git a/src/test/java/org/opensearch/ad/ADIntegTestCase.java b/src/test/java/org/opensearch/ad/ADIntegTestCase.java index 0ad57e304..cff2a5619 100644 --- a/src/test/java/org/opensearch/ad/ADIntegTestCase.java +++ b/src/test/java/org/opensearch/ad/ADIntegTestCase.java @@ -136,6 +136,10 @@ public void createADResultIndex() throws IOException { createIndex(CommonName.ANOMALY_RESULT_INDEX_ALIAS, AnomalyDetectionIndices.getAnomalyResultMappings()); } + public void createCustomADResultIndex(String indexName) throws IOException { + createIndex(indexName, AnomalyDetectionIndices.getAnomalyResultMappings()); + } + public void createDetectionStateIndex() throws IOException { createIndex(CommonName.DETECTION_STATE_INDEX, AnomalyDetectionIndices.getDetectionStateMappings()); } diff --git a/src/test/java/org/opensearch/ad/TestHelpers.java b/src/test/java/org/opensearch/ad/TestHelpers.java index 7e833e5e2..796aff104 100644 --- a/src/test/java/org/opensearch/ad/TestHelpers.java +++ b/src/test/java/org/opensearch/ad/TestHelpers.java @@ -1440,11 +1440,20 @@ public static Map parseStatsResult(String statsResult) throws IO } public static DetectorValidationIssue randomDetectorValidationIssue() { + DetectorValidationIssue issue = new DetectorValidationIssue( + ValidationAspect.DETECTOR, + DetectorValidationIssueType.NAME, + randomAlphaOfLength(5) + ); + return issue; + } + + public static DetectorValidationIssue randomDetectorValidationIssueWithSubIssues(Map subIssues) { DetectorValidationIssue issue = new DetectorValidationIssue( ValidationAspect.DETECTOR, DetectorValidationIssueType.NAME, randomAlphaOfLength(5), - null, + subIssues, null ); return issue; diff --git a/src/test/java/org/opensearch/ad/model/ADEntityTaskProfileTests.java b/src/test/java/org/opensearch/ad/model/ADEntityTaskProfileTests.java new file mode 100644 index 000000000..304f7f2d8 --- /dev/null +++ b/src/test/java/org/opensearch/ad/model/ADEntityTaskProfileTests.java @@ -0,0 +1,96 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.ad.model; + +import java.io.IOException; +import java.util.Collection; +import java.util.TreeMap; + +import org.opensearch.ad.AnomalyDetectorPlugin; +import org.opensearch.ad.TestHelpers; +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.common.io.stream.NamedWriteableAwareStreamInput; +import org.opensearch.common.io.stream.NamedWriteableRegistry; +import org.opensearch.common.xcontent.ToXContent; +import org.opensearch.plugins.Plugin; +import org.opensearch.test.InternalSettingsPlugin; +import org.opensearch.test.OpenSearchSingleNodeTestCase; + +public class ADEntityTaskProfileTests extends OpenSearchSingleNodeTestCase { + + @Override + protected Collection> getPlugins() { + return pluginList(InternalSettingsPlugin.class, AnomalyDetectorPlugin.class); + } + + @Override + protected NamedWriteableRegistry writableRegistry() { + return getInstanceFromNode(NamedWriteableRegistry.class); + } + + public void testADEntityTaskProfileSerialization() throws IOException { + ADEntityTaskProfile entityTask = new ADEntityTaskProfile( + 1, + 23L, + false, + 1, + 2L, + "1234", + null, + "4321", + ADTaskType.HISTORICAL_HC_ENTITY.name() + ); + BytesStreamOutput output = new BytesStreamOutput(); + entityTask.writeTo(output); + NamedWriteableAwareStreamInput input = new NamedWriteableAwareStreamInput(output.bytes().streamInput(), writableRegistry()); + ADEntityTaskProfile parsedEntityTask = new ADEntityTaskProfile(input); + assertEquals(entityTask, parsedEntityTask); + } + + public void testParseADEntityTaskProfile() throws IOException { + TreeMap attributes = new TreeMap<>(); + String name1 = "host"; + String val1 = "server_2"; + String name2 = "service"; + String val2 = "app_4"; + attributes.put(name1, val1); + attributes.put(name2, val2); + Entity entity = Entity.createEntityFromOrderedMap(attributes); + ADEntityTaskProfile entityTask = new ADEntityTaskProfile( + 1, + 23L, + false, + 1, + 2L, + "1234", + entity, + "4321", + ADTaskType.HISTORICAL_HC_ENTITY.name() + ); + String adEntityTaskProfileString = TestHelpers + .xContentBuilderToString(entityTask.toXContent(TestHelpers.builder(), ToXContent.EMPTY_PARAMS)); + ADEntityTaskProfile parsedEntityTask = ADEntityTaskProfile.parse(TestHelpers.parser(adEntityTaskProfileString)); + assertEquals(entityTask, parsedEntityTask); + } + + public void testParseADEntityTaskProfileWithNullEntity() throws IOException { + ADEntityTaskProfile entityTask = new ADEntityTaskProfile( + 1, + 23L, + false, + 1, + 2L, + "1234", + null, + "4321", + ADTaskType.HISTORICAL_HC_ENTITY.name() + ); + String adEntityTaskProfileString = TestHelpers + .xContentBuilderToString(entityTask.toXContent(TestHelpers.builder(), ToXContent.EMPTY_PARAMS)); + ADEntityTaskProfile parsedEntityTask = ADEntityTaskProfile.parse(TestHelpers.parser(adEntityTaskProfileString)); + assertEquals(entityTask, parsedEntityTask); + } +} diff --git a/src/test/java/org/opensearch/ad/model/AnomalyDetectorTests.java b/src/test/java/org/opensearch/ad/model/AnomalyDetectorTests.java index e1e2ba43e..d96a3435c 100644 --- a/src/test/java/org/opensearch/ad/model/AnomalyDetectorTests.java +++ b/src/test/java/org/opensearch/ad/model/AnomalyDetectorTests.java @@ -26,6 +26,7 @@ import org.opensearch.ad.AbstractADTest; import org.opensearch.ad.TestHelpers; import org.opensearch.ad.common.exception.ADValidationException; +import org.opensearch.ad.constant.CommonErrorMessages; import org.opensearch.ad.constant.CommonName; import org.opensearch.ad.settings.AnomalyDetectorSettings; import org.opensearch.common.unit.TimeValue; @@ -67,6 +68,23 @@ public void testParseAnomalyDetectorWithCustomIndex() throws IOException { assertEquals("Parsing anomaly detector doesn't work", detector, parsedDetector); } + public void testAnomalyDetectorWithInvalidCustomIndex() throws Exception { + String resultIndex = CommonName.CUSTOM_RESULT_INDEX_PREFIX + "test@@"; + TestHelpers + .assertFailWith( + ADValidationException.class, + () -> (TestHelpers + .randomDetector( + ImmutableList.of(TestHelpers.randomFeature()), + randomAlphaOfLength(5), + randomIntBetween(1, 5), + randomAlphaOfLength(5), + ImmutableList.of(randomAlphaOfLength(5)), + resultIndex + )) + ); + } + public void testParseAnomalyDetectorWithoutParams() throws IOException { AnomalyDetector detector = TestHelpers.randomAnomalyDetector(TestHelpers.randomUiMetadata(), Instant.now()); String detectorString = TestHelpers.xContentBuilderToString(detector.toXContent(TestHelpers.builder())); @@ -229,6 +247,44 @@ public void testParseAnomalyDetectorWithIncorrectFeatureQuery() throws Exception TestHelpers.assertFailWith(ADValidationException.class, () -> AnomalyDetector.parse(TestHelpers.parser(detectorString))); } + public void testParseAnomalyDetectorWithInvalidDetectorIntervalUnits() { + String detectorString = "{\"name\":\"todagtCMkwpcaedpyYUM\",\"description\":" + + "\"ClrcaMpuLfeDSlVduRcKlqPZyqWDBf\",\"time_field\":\"dJRwh\",\"indices\":[\"eIrgWMqAED\"]," + + "\"feature_attributes\":[{\"feature_id\":\"lxYRN\",\"feature_name\":\"eqSeU\",\"feature_enabled\"" + + ":true,\"aggregation_query\":{\"aa\":{\"value_count\":{\"field\":\"ok\"}}}}],\"detection_interval\":" + + "{\"period\":{\"interval\":425,\"unit\":\"Millis\"}},\"window_delay\":{\"period\":{\"interval\":973," + + "\"unit\":\"Minutes\"}},\"shingle_size\":4,\"schema_version\":-1203962153,\"ui_metadata\":{\"JbAaV\":{\"feature_id\":" + + "\"rIFjS\",\"feature_name\":\"QXCmS\",\"feature_enabled\":false,\"aggregation_query\":{\"aa\":" + + "{\"value_count\":{\"field\":\"ok\"}}}}},\"last_update_time\":1568396089028}"; + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> AnomalyDetector.parse(TestHelpers.parser(detectorString)) + ); + assertEquals( + String.format(Locale.ROOT, CommonErrorMessages.INVALID_TIME_CONFIGURATION_UNITS, ChronoUnit.MILLIS), + exception.getMessage() + ); + } + + public void testParseAnomalyDetectorInvalidWindowDelayUnits() { + String detectorString = "{\"name\":\"todagtCMkwpcaedpyYUM\",\"description\":" + + "\"ClrcaMpuLfeDSlVduRcKlqPZyqWDBf\",\"time_field\":\"dJRwh\",\"indices\":[\"eIrgWMqAED\"]," + + "\"feature_attributes\":[{\"feature_id\":\"lxYRN\",\"feature_name\":\"eqSeU\",\"feature_enabled\"" + + ":true,\"aggregation_query\":{\"aa\":{\"value_count\":{\"field\":\"ok\"}}}}],\"detection_interval\":" + + "{\"period\":{\"interval\":425,\"unit\":\"Minutes\"}},\"window_delay\":{\"period\":{\"interval\":973," + + "\"unit\":\"Millis\"}},\"shingle_size\":4,\"schema_version\":-1203962153,\"ui_metadata\":{\"JbAaV\":{\"feature_id\":" + + "\"rIFjS\",\"feature_name\":\"QXCmS\",\"feature_enabled\":false,\"aggregation_query\":{\"aa\":" + + "{\"value_count\":{\"field\":\"ok\"}}}}},\"last_update_time\":1568396089028}"; + IllegalArgumentException exception = expectThrows( + IllegalArgumentException.class, + () -> AnomalyDetector.parse(TestHelpers.parser(detectorString)) + ); + assertEquals( + String.format(Locale.ROOT, CommonErrorMessages.INVALID_TIME_CONFIGURATION_UNITS, ChronoUnit.MILLIS), + exception.getMessage() + ); + } + public void testParseAnomalyDetectorWithNullUiMetadata() throws IOException { AnomalyDetector detector = TestHelpers.randomAnomalyDetector(null, Instant.now()); String detectorString = TestHelpers.xContentBuilderToString(detector.toXContent(TestHelpers.builder(), ToXContent.EMPTY_PARAMS)); diff --git a/src/test/java/org/opensearch/ad/model/DetectorInternalStateTests.java b/src/test/java/org/opensearch/ad/model/DetectorInternalStateTests.java new file mode 100644 index 000000000..a91721db5 --- /dev/null +++ b/src/test/java/org/opensearch/ad/model/DetectorInternalStateTests.java @@ -0,0 +1,27 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.ad.model; + +import java.io.IOException; +import java.time.Instant; + +import org.opensearch.ad.TestHelpers; +import org.opensearch.common.xcontent.ToXContent; +import org.opensearch.test.OpenSearchSingleNodeTestCase; + +public class DetectorInternalStateTests extends OpenSearchSingleNodeTestCase { + + public void testToXContentDetectorInternalState() throws IOException { + DetectorInternalState internalState = new DetectorInternalState.Builder() + .lastUpdateTime(Instant.ofEpochMilli(100L)) + .error("error-test") + .build(); + String internalStateString = TestHelpers + .xContentBuilderToString(internalState.toXContent(TestHelpers.builder(), ToXContent.EMPTY_PARAMS)); + DetectorInternalState parsedInternalState = DetectorInternalState.parse(TestHelpers.parser(internalStateString)); + assertEquals(internalState, parsedInternalState); + } +} diff --git a/src/test/java/org/opensearch/ad/model/DetectorProfileTests.java b/src/test/java/org/opensearch/ad/model/DetectorProfileTests.java index eed3ace49..10c41d685 100644 --- a/src/test/java/org/opensearch/ad/model/DetectorProfileTests.java +++ b/src/test/java/org/opensearch/ad/model/DetectorProfileTests.java @@ -12,17 +12,20 @@ package org.opensearch.ad.model; import java.io.IOException; +import java.util.Map; +import org.opensearch.ad.TestHelpers; +import org.opensearch.ad.constant.CommonErrorMessages; +import org.opensearch.ad.constant.CommonName; import org.opensearch.common.io.stream.BytesStreamOutput; import org.opensearch.common.io.stream.NamedWriteableAwareStreamInput; +import org.opensearch.common.xcontent.XContentParser; import org.opensearch.test.OpenSearchTestCase; public class DetectorProfileTests extends OpenSearchTestCase { - public void testParseDetectorProfile() throws IOException { - String detectorId = randomAlphaOfLength(10); - String[] runningEntities = new String[] { randomAlphaOfLength(5) }; - DetectorProfile detectorProfile = new DetectorProfile.Builder() + private DetectorProfile createRandomDetectorProfile() { + return new DetectorProfile.Builder() .state(DetectorState.INIT) .error(randomAlphaOfLength(5)) .modelProfile( @@ -52,12 +55,40 @@ public void testParseDetectorProfile() throws IOException { ) ) .build(); + } + public void testParseDetectorProfile() throws IOException { + DetectorProfile detectorProfile = createRandomDetectorProfile(); BytesStreamOutput output = new BytesStreamOutput(); detectorProfile.writeTo(output); NamedWriteableAwareStreamInput input = new NamedWriteableAwareStreamInput(output.bytes().streamInput(), writableRegistry()); - // StreamInput input = output.bytes().streamInput(); DetectorProfile parsedDetectorProfile = new DetectorProfile(input); assertEquals("Detector profile serialization doesn't work", detectorProfile, parsedDetectorProfile); } + + public void testMergeDetectorProfile() { + DetectorProfile detectorProfileOne = createRandomDetectorProfile(); + DetectorProfile detectorProfileTwo = createRandomDetectorProfile(); + String errorPreMerge = detectorProfileOne.getError(); + detectorProfileOne.merge(detectorProfileTwo); + assertTrue(detectorProfileOne.toString().contains(detectorProfileTwo.getError())); + assertFalse(detectorProfileOne.toString().contains(errorPreMerge)); + assertTrue(detectorProfileOne.toString().contains(detectorProfileTwo.getCoordinatingNode())); + } + + public void testDetectorProfileToXContent() throws IOException { + DetectorProfile detectorProfile = createRandomDetectorProfile(); + String detectorProfileString = TestHelpers.xContentBuilderToString(detectorProfile.toXContent(TestHelpers.builder())); + XContentParser parser = TestHelpers.parser(detectorProfileString); + Map parsedMap = parser.map(); + assertEquals(detectorProfile.getCoordinatingNode(), parsedMap.get("coordinating_node")); + assertEquals(detectorProfile.getState().toString(), parsedMap.get("state")); + assertTrue(parsedMap.get("models").toString().contains(detectorProfile.getModelProfile()[0].getModelId())); + } + + public void testDetectorProfileName() throws IllegalArgumentException { + DetectorProfileName.getName(CommonName.AD_TASK); + IllegalArgumentException exception = expectThrows(IllegalArgumentException.class, () -> DetectorProfileName.getName("abc")); + assertEquals(exception.getMessage(), CommonErrorMessages.UNSUPPORTED_PROFILE_TYPE); + } } diff --git a/src/test/java/org/opensearch/ad/model/EntityProfileTests.java b/src/test/java/org/opensearch/ad/model/EntityProfileTests.java index d3141e64e..b55ecc880 100644 --- a/src/test/java/org/opensearch/ad/model/EntityProfileTests.java +++ b/src/test/java/org/opensearch/ad/model/EntityProfileTests.java @@ -32,6 +32,7 @@ public void testMerge() { profile1.merge(profile2); assertEquals(profile1.getState(), EntityState.INIT); + assertTrue(profile1.toString().contains(EntityState.INIT.toString())); } public void testToXContent() throws IOException, JsonPathNotFoundException { diff --git a/src/test/java/org/opensearch/ad/transport/ADTaskProfileTests.java b/src/test/java/org/opensearch/ad/transport/ADTaskProfileTests.java index 303bc662e..a71025acb 100644 --- a/src/test/java/org/opensearch/ad/transport/ADTaskProfileTests.java +++ b/src/test/java/org/opensearch/ad/transport/ADTaskProfileTests.java @@ -31,6 +31,7 @@ import org.opensearch.common.io.stream.BytesStreamOutput; import org.opensearch.common.io.stream.NamedWriteableAwareStreamInput; import org.opensearch.common.io.stream.NamedWriteableRegistry; +import org.opensearch.common.xcontent.ToXContent; import org.opensearch.plugins.Plugin; import org.opensearch.test.InternalSettingsPlugin; import org.opensearch.test.OpenSearchSingleNodeTestCase; @@ -108,11 +109,28 @@ private void testADTaskProfileResponse(ADTaskProfileNodeResponse response) throw response.writeTo(output); NamedWriteableAwareStreamInput input = new NamedWriteableAwareStreamInput(output.bytes().streamInput(), writableRegistry()); ADTaskProfileNodeResponse parsedResponse = ADTaskProfileNodeResponse.readNodeResponse(input); - // if (response.getAdTaskProfile() != null) { - // assertTrue(response.getAdTaskProfile().equals(parsedResponse.getAdTaskProfile())); - // } else { - // assertNull(parsedResponse.getAdTaskProfile()); - // } + if (response.getAdTaskProfile() != null) { + assertTrue(response.getAdTaskProfile().equals(parsedResponse.getAdTaskProfile())); + } else { + assertNull(parsedResponse.getAdTaskProfile()); + } + } + + public void testADTaskProfileParse() throws IOException { + ADTaskProfile adTaskProfile = new ADTaskProfile( + randomAlphaOfLength(5), + randomInt(), + randomLong(), + randomBoolean(), + randomInt(), + randomLong(), + randomAlphaOfLength(5) + ); + String adTaskProfileString = TestHelpers + .xContentBuilderToString(adTaskProfile.toXContent(TestHelpers.builder(), ToXContent.EMPTY_PARAMS)); + ADTaskProfile parsedADTaskProfile = ADTaskProfile.parse(TestHelpers.parser(adTaskProfileString)); + assertEquals(adTaskProfile, parsedADTaskProfile); + assertEquals(parsedADTaskProfile.toString(), adTaskProfile.toString()); } @Ignore diff --git a/src/test/java/org/opensearch/ad/transport/GetAnomalyDetectorTransportActionTests.java b/src/test/java/org/opensearch/ad/transport/GetAnomalyDetectorTransportActionTests.java index 793665051..544ad1ce0 100644 --- a/src/test/java/org/opensearch/ad/transport/GetAnomalyDetectorTransportActionTests.java +++ b/src/test/java/org/opensearch/ad/transport/GetAnomalyDetectorTransportActionTests.java @@ -233,6 +233,7 @@ public void testGetAnomalyDetectorProfileResponse() throws IOException { Map map = TestHelpers.XContentBuilderToMap(builder); Map parsedInitProgress = (Map) (map.get(CommonName.INIT_PROGRESS)); Assert.assertEquals(initProgress.getPercentage(), parsedInitProgress.get(InitProgressProfile.PERCENTAGE).toString()); + assertTrue(initProgress.toString().contains("[percentage=99%,estimated_minutes_left=2,needed_shingles=2]")); Assert .assertEquals( String.valueOf(initProgress.getEstimatedMinutesLeft()), diff --git a/src/test/java/org/opensearch/ad/transport/ValidateAnomalyDetectorResponseTests.java b/src/test/java/org/opensearch/ad/transport/ValidateAnomalyDetectorResponseTests.java index 91dd2eda3..5082a6e93 100644 --- a/src/test/java/org/opensearch/ad/transport/ValidateAnomalyDetectorResponseTests.java +++ b/src/test/java/org/opensearch/ad/transport/ValidateAnomalyDetectorResponseTests.java @@ -12,6 +12,8 @@ package org.opensearch.ad.transport; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import org.junit.Test; import org.opensearch.ad.AbstractADTest; @@ -19,20 +21,20 @@ import org.opensearch.ad.model.DetectorValidationIssue; import org.opensearch.common.io.stream.BytesStreamOutput; import org.opensearch.common.io.stream.StreamInput; -import org.opensearch.common.xcontent.ToXContent; public class ValidateAnomalyDetectorResponseTests extends AbstractADTest { @Test public void testResponseSerialization() throws IOException { - DetectorValidationIssue issue = TestHelpers.randomDetectorValidationIssue(); + Map subIssues = new HashMap<>(); + subIssues.put("a", "b"); + subIssues.put("c", "d"); + DetectorValidationIssue issue = TestHelpers.randomDetectorValidationIssueWithSubIssues(subIssues); ValidateAnomalyDetectorResponse response = new ValidateAnomalyDetectorResponse(issue); BytesStreamOutput output = new BytesStreamOutput(); response.writeTo(output); - StreamInput streamInput = output.bytes().streamInput(); ValidateAnomalyDetectorResponse readResponse = ValidateAnomalyDetectorAction.INSTANCE.getResponseReader().read(streamInput); - assertEquals("serialization has the wrong issue", issue, readResponse.getIssue()); } @@ -41,19 +43,36 @@ public void testResponseSerializationWithEmptyIssue() throws IOException { ValidateAnomalyDetectorResponse response = new ValidateAnomalyDetectorResponse((DetectorValidationIssue) null); BytesStreamOutput output = new BytesStreamOutput(); response.writeTo(output); - StreamInput streamInput = output.bytes().streamInput(); ValidateAnomalyDetectorResponse readResponse = ValidateAnomalyDetectorAction.INSTANCE.getResponseReader().read(streamInput); - assertNull("serialization should have empty issue", readResponse.getIssue()); } + public void testResponseToXContentWithSubIssues() throws IOException { + Map subIssues = new HashMap<>(); + subIssues.put("a", "b"); + subIssues.put("c", "d"); + DetectorValidationIssue issue = TestHelpers.randomDetectorValidationIssueWithSubIssues(subIssues); + ValidateAnomalyDetectorResponse response = new ValidateAnomalyDetectorResponse(issue); + String validationResponse = TestHelpers.xContentBuilderToString(response.toXContent(TestHelpers.builder())); + String message = issue.getMessage(); + assertEquals( + "{\"detector\":{\"name\":{\"message\":\"" + message + "\",\"sub_issues\":{\"a\":\"b\",\"c\":\"d\"}}}}", + validationResponse + ); + } + public void testResponseToXContent() throws IOException { DetectorValidationIssue issue = TestHelpers.randomDetectorValidationIssue(); ValidateAnomalyDetectorResponse response = new ValidateAnomalyDetectorResponse(issue); - String validationResponse = TestHelpers - .xContentBuilderToString(response.toXContent(TestHelpers.builder(), ToXContent.EMPTY_PARAMS)); + String validationResponse = TestHelpers.xContentBuilderToString(response.toXContent(TestHelpers.builder())); String message = issue.getMessage(); assertEquals("{\"detector\":{\"name\":{\"message\":\"" + message + "\"}}}", validationResponse); } + + public void testResponseToXContentNull() throws IOException { + ValidateAnomalyDetectorResponse response = new ValidateAnomalyDetectorResponse((DetectorValidationIssue) null); + String validationResponse = TestHelpers.xContentBuilderToString(response.toXContent(TestHelpers.builder())); + assertEquals("{}", validationResponse); + } } diff --git a/src/test/java/org/opensearch/ad/transport/ValidateAnomalyDetectorTransportActionTests.java b/src/test/java/org/opensearch/ad/transport/ValidateAnomalyDetectorTransportActionTests.java index a03b1015d..d694389a5 100644 --- a/src/test/java/org/opensearch/ad/transport/ValidateAnomalyDetectorTransportActionTests.java +++ b/src/test/java/org/opensearch/ad/transport/ValidateAnomalyDetectorTransportActionTests.java @@ -16,6 +16,7 @@ import static org.opensearch.ad.rest.handler.AbstractAnomalyDetectorActionHandler.UNKNOWN_SEARCH_QUERY_EXCEPTION_MSG; import java.io.IOException; +import java.net.URL; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -23,15 +24,20 @@ import org.opensearch.ad.ADIntegTestCase; import org.opensearch.ad.TestHelpers; import org.opensearch.ad.constant.CommonErrorMessages; +import org.opensearch.ad.constant.CommonName; +import org.opensearch.ad.indices.AnomalyDetectionIndices; import org.opensearch.ad.model.AnomalyDetector; import org.opensearch.ad.model.DetectorValidationIssueType; import org.opensearch.ad.model.Feature; import org.opensearch.ad.model.ValidationAspect; +import org.opensearch.ad.settings.AnomalyDetectorSettings; import org.opensearch.common.unit.TimeValue; import org.opensearch.search.aggregations.AggregationBuilder; +import com.google.common.base.Charsets; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.io.Resources; public class ValidateAnomalyDetectorTransportActionTests extends ADIntegTestCase { @@ -55,8 +61,6 @@ public void testValidateAnomalyDetectorWithNoIssue() throws IOException { @Test public void testValidateAnomalyDetectorWithNoIndexFound() throws IOException { AnomalyDetector anomalyDetector = TestHelpers.randomAnomalyDetector(ImmutableMap.of(), Instant.now()); - Instant startTime = Instant.now().minus(1, ChronoUnit.DAYS); - // ingestTestDataValidate(anomalyDetector.getIndices().get(0), startTime, 1, "error"); ValidateAnomalyDetectorRequest request = new ValidateAnomalyDetectorRequest( anomalyDetector, ValidationAspect.DETECTOR.getName(), @@ -261,4 +265,174 @@ public void testValidateAnomalyDetectorWithMultipleInvalidFeatureField() throws assertTrue(response.getIssue().getSubIssues().containsKey(maxFeature.getName())); assertTrue(FEATURE_WITH_INVALID_QUERY_MSG.contains(response.getIssue().getSubIssues().get(maxFeature.getName()))); } + + @Test + public void testValidateAnomalyDetectorWithCustomResultIndex() throws IOException { + String resultIndex = CommonName.CUSTOM_RESULT_INDEX_PREFIX + "test"; + createCustomADResultIndex(resultIndex); + AnomalyDetector anomalyDetector = TestHelpers + .randomDetector( + ImmutableList.of(TestHelpers.randomFeature()), + randomAlphaOfLength(5).toLowerCase(), + randomIntBetween(1, 5), + randomAlphaOfLength(5), + null, + resultIndex + ); + Instant startTime = Instant.now().minus(1, ChronoUnit.DAYS); + ingestTestDataValidate(anomalyDetector.getIndices().get(0), startTime, 1, "error"); + ValidateAnomalyDetectorRequest request = new ValidateAnomalyDetectorRequest( + anomalyDetector, + ValidationAspect.DETECTOR.getName(), + 5, + 5, + 5, + new TimeValue(5_000L) + ); + ValidateAnomalyDetectorResponse response = client().execute(ValidateAnomalyDetectorAction.INSTANCE, request).actionGet(5_000); + assertNull(response.getIssue()); + } + + @Test + public void testValidateAnomalyDetectorWithCustomResultIndexCreated() throws IOException { + testValidateAnomalyDetectorWithCustomResultIndex(true); + } + + @Test + public void testValidateAnomalyDetectorWithCustomResultIndexPresentButNotCreated() throws IOException { + testValidateAnomalyDetectorWithCustomResultIndex(false); + + } + + @Test + public void testValidateAnomalyDetectorWithCustomResultIndexWithInvalidMapping() throws IOException { + String resultIndex = CommonName.CUSTOM_RESULT_INDEX_PREFIX + "test"; + URL url = AnomalyDetectionIndices.class.getClassLoader().getResource("mappings/checkpoint.json"); + createIndex(resultIndex, Resources.toString(url, Charsets.UTF_8)); + AnomalyDetector anomalyDetector = TestHelpers + .randomDetector( + ImmutableList.of(TestHelpers.randomFeature()), + randomAlphaOfLength(5).toLowerCase(), + randomIntBetween(1, 5), + randomAlphaOfLength(5), + null, + resultIndex + ); + Instant startTime = Instant.now().minus(1, ChronoUnit.DAYS); + ingestTestDataValidate(anomalyDetector.getIndices().get(0), startTime, 1, "error"); + ValidateAnomalyDetectorRequest request = new ValidateAnomalyDetectorRequest( + anomalyDetector, + ValidationAspect.DETECTOR.getName(), + 5, + 5, + 5, + new TimeValue(5_000L) + ); + ValidateAnomalyDetectorResponse response = client().execute(ValidateAnomalyDetectorAction.INSTANCE, request).actionGet(5_000); + assertEquals(DetectorValidationIssueType.RESULT_INDEX, response.getIssue().getType()); + assertEquals(ValidationAspect.DETECTOR, response.getIssue().getAspect()); + assertTrue(response.getIssue().getMessage().contains(CommonErrorMessages.INVALID_RESULT_INDEX_MAPPING)); + } + + private void testValidateAnomalyDetectorWithCustomResultIndex(boolean resultIndexCreated) throws IOException { + String resultIndex = CommonName.CUSTOM_RESULT_INDEX_PREFIX + "test"; + if (resultIndexCreated) { + createCustomADResultIndex(resultIndex); + } + AnomalyDetector anomalyDetector = TestHelpers + .randomDetector( + ImmutableList.of(TestHelpers.randomFeature()), + randomAlphaOfLength(5).toLowerCase(), + randomIntBetween(1, 5), + randomAlphaOfLength(5), + null, + resultIndex + ); + Instant startTime = Instant.now().minus(1, ChronoUnit.DAYS); + ingestTestDataValidate(anomalyDetector.getIndices().get(0), startTime, 1, "error"); + ValidateAnomalyDetectorRequest request = new ValidateAnomalyDetectorRequest( + anomalyDetector, + ValidationAspect.DETECTOR.getName(), + 5, + 5, + 5, + new TimeValue(5_000L) + ); + ValidateAnomalyDetectorResponse response = client().execute(ValidateAnomalyDetectorAction.INSTANCE, request).actionGet(5_000); + assertNull(response.getIssue()); + } + + @Test + public void testValidateAnomalyDetectorWithInvalidDetectorName() throws IOException { + AnomalyDetector anomalyDetector = new AnomalyDetector( + randomAlphaOfLength(5), + randomLong(), + "#$32", + randomAlphaOfLength(5), + randomAlphaOfLength(5), + ImmutableList.of(randomAlphaOfLength(5).toLowerCase()), + ImmutableList.of(TestHelpers.randomFeature()), + TestHelpers.randomQuery(), + TestHelpers.randomIntervalTimeConfiguration(), + TestHelpers.randomIntervalTimeConfiguration(), + AnomalyDetectorSettings.DEFAULT_SHINGLE_SIZE, + null, + 1, + Instant.now(), + null, + TestHelpers.randomUser(), + null + ); + Instant startTime = Instant.now().minus(1, ChronoUnit.DAYS); + ingestTestDataValidate(anomalyDetector.getIndices().get(0), startTime, 1, "error"); + ValidateAnomalyDetectorRequest request = new ValidateAnomalyDetectorRequest( + anomalyDetector, + ValidationAspect.DETECTOR.getName(), + 5, + 5, + 5, + new TimeValue(5_000L) + ); + ValidateAnomalyDetectorResponse response = client().execute(ValidateAnomalyDetectorAction.INSTANCE, request).actionGet(5_000); + assertEquals(DetectorValidationIssueType.NAME, response.getIssue().getType()); + assertEquals(ValidationAspect.DETECTOR, response.getIssue().getAspect()); + assertEquals(CommonErrorMessages.INVALID_DETECTOR_NAME, response.getIssue().getMessage()); + } + + @Test + public void testValidateAnomalyDetectorWithDetectorNameTooLong() throws IOException { + AnomalyDetector anomalyDetector = new AnomalyDetector( + randomAlphaOfLength(5), + randomLong(), + "abababababababababababababababababababababababababababababababababababababababababababababababab", + randomAlphaOfLength(5), + randomAlphaOfLength(5), + ImmutableList.of(randomAlphaOfLength(5).toLowerCase()), + ImmutableList.of(TestHelpers.randomFeature()), + TestHelpers.randomQuery(), + TestHelpers.randomIntervalTimeConfiguration(), + TestHelpers.randomIntervalTimeConfiguration(), + AnomalyDetectorSettings.DEFAULT_SHINGLE_SIZE, + null, + 1, + Instant.now(), + null, + TestHelpers.randomUser(), + null + ); + Instant startTime = Instant.now().minus(1, ChronoUnit.DAYS); + ingestTestDataValidate(anomalyDetector.getIndices().get(0), startTime, 1, "error"); + ValidateAnomalyDetectorRequest request = new ValidateAnomalyDetectorRequest( + anomalyDetector, + ValidationAspect.DETECTOR.getName(), + 5, + 5, + 5, + new TimeValue(5_000L) + ); + ValidateAnomalyDetectorResponse response = client().execute(ValidateAnomalyDetectorAction.INSTANCE, request).actionGet(5_000); + assertEquals(DetectorValidationIssueType.NAME, response.getIssue().getType()); + assertEquals(ValidationAspect.DETECTOR, response.getIssue().getAspect()); + assertTrue(response.getIssue().getMessage().contains("Name should be shortened. The maximum limit is")); + } } diff --git a/src/test/java/org/opensearch/ad/util/ParseUtilsTests.java b/src/test/java/org/opensearch/ad/util/ParseUtilsTests.java index 54ad0a522..1e354f19e 100644 --- a/src/test/java/org/opensearch/ad/util/ParseUtilsTests.java +++ b/src/test/java/org/opensearch/ad/util/ParseUtilsTests.java @@ -21,6 +21,7 @@ import org.opensearch.ad.common.exception.AnomalyDetectionException; import org.opensearch.ad.model.AnomalyDetector; import org.opensearch.ad.model.Feature; +import org.opensearch.common.ParsingException; import org.opensearch.common.xcontent.XContentBuilder; import org.opensearch.common.xcontent.XContentFactory; import org.opensearch.common.xcontent.XContentParser; @@ -85,6 +86,26 @@ public void testParseAggregatorsWithAggregationQueryString() throws IOException assertEquals("test", agg.getAggregatorFactories().iterator().next().getName()); } + public void testParseAggregatorsWithInvalidAggregationName() throws IOException { + XContentParser parser = ParseUtils.parser("{\"aa\":{\"value_count\":{\"field\":\"ok\"}}}", TestHelpers.xContentRegistry()); + Exception ex = expectThrows(ParsingException.class, () -> ParseUtils.parseAggregators(parser, 0, "#@?><:{")); + assertTrue(ex.getMessage().contains("Aggregation names must be alpha-numeric and can only contain '_' and '-'")); + } + + public void testParseAggregatorsWithTwoAggregationTypes() throws IOException { + XContentParser parser = ParseUtils + .parser("{\"test\":{\"avg\":{\"field\":\"value\"},\"sum\":{\"field\":\"value\"}}}", TestHelpers.xContentRegistry()); + Exception ex = expectThrows(ParsingException.class, () -> ParseUtils.parseAggregators(parser, 0, "test")); + assertTrue(ex.getMessage().contains("Found two aggregation type definitions in")); + } + + public void testParseAggregatorsWithNullAggregationDefinition() throws IOException { + String aggName = "test"; + XContentParser parser = ParseUtils.parser("{\"test\":{}}", TestHelpers.xContentRegistry()); + Exception ex = expectThrows(ParsingException.class, () -> ParseUtils.parseAggregators(parser, 0, aggName)); + assertTrue(ex.getMessage().contains("Missing definition for aggregation [" + aggName + "]")); + } + public void testParseAggregatorsWithAggregationQueryStringAndNullAggName() throws IOException { AggregatorFactories.Builder agg = ParseUtils .parseAggregators("{\"aa\":{\"value_count\":{\"field\":\"ok\"}}}", TestHelpers.xContentRegistry(), null);