diff --git a/server/src/test/java/org/elasticsearch/index/reindex/BasicHitTests.java b/server/src/test/java/org/elasticsearch/index/reindex/BasicHitTests.java new file mode 100644 index 0000000000000..24cf3b8ecd2b4 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/reindex/BasicHitTests.java @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.reindex; + +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.index.reindex.PaginatedHitSource.BasicHit; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.XContentType; + +public class BasicHitTests extends ESTestCase { + + /** + * Verifies that index, id, and version provided at construction are returned unchanged by their respective getters. + * Verifies that optional fields are null or zero by default before any setters are invoked. + */ + public void testConstructor() { + String index = randomAlphaOfLengthBetween(3, 10); + String id = randomAlphaOfLengthBetween(3, 10); + long version = randomNonNegativeLong(); + PaginatedHitSource.BasicHit hit = new BasicHit(index, id, version); + + assertEquals(index, hit.getIndex()); + assertEquals(id, hit.getId()); + assertEquals(version, hit.getVersion()); + + assertNull(hit.getSource()); + assertNull(hit.getXContentType()); + assertNull(hit.getRouting()); + assertEquals(0L, hit.getSeqNo()); + assertEquals(0L, hit.getPrimaryTerm()); + } + + /** + * Verifies that setSource correctly sets both the source bytes and the associated XContentType. + * Verifies that setSource returns the same instance, allowing fluent-style method chaining. + */ + public void testSetSource() { + BasicHit hit = new BasicHit(randomAlphaOfLengthBetween(3, 10), randomAlphaOfLengthBetween(3, 10), randomNonNegativeLong()); + BytesReference source = new BytesArray(randomAlphaOfLengthBetween(5, 50)); + XContentType xContentType = randomFrom(XContentType.values()); + + BasicHit returned = hit.setSource(source, xContentType); + assertSame(source, hit.getSource()); + assertEquals(xContentType, hit.getXContentType()); + assertSame(hit, returned); + } + + /** + * Verifies that routing can be set and retrieved correctly. + * Verifies that setRouting returns the same instance, allowing fluent-style chaining. + */ + public void testSetRouting() { + BasicHit hit = new BasicHit(randomAlphaOfLengthBetween(3, 10), randomAlphaOfLengthBetween(3, 10), randomNonNegativeLong()); + String routing = randomAlphaOfLengthBetween(3, 20); + BasicHit returned = hit.setRouting(routing); + assertEquals(routing, hit.getRouting()); + assertSame(hit, returned); + } + + /** + * Verifies that sequence number can be set and retrieved correctly. + */ + public void testSetSeqNo() { + BasicHit hit = new BasicHit(randomAlphaOfLengthBetween(3, 10), randomAlphaOfLengthBetween(3, 10), randomNonNegativeLong()); + long seqNo = randomNonNegativeLong(); + hit.setSeqNo(seqNo); + assertEquals(seqNo, hit.getSeqNo()); + } + + /** + * Verifies that primary term can be set and retrieved correctly. + */ + public void testSetPrimaryTerm() { + BasicHit hit = new BasicHit(randomAlphaOfLengthBetween(3, 10), randomAlphaOfLengthBetween(3, 10), randomNonNegativeLong()); + long primaryTerm = randomNonNegativeLong(); + hit.setPrimaryTerm(primaryTerm); + assertEquals(primaryTerm, hit.getPrimaryTerm()); + } + + /** + * Verifies that setting all optional fields does not affect the required constructor-provided fields. + */ + public void testOptionalSettersDoNotAffectRequiredFields() { + String index = randomAlphaOfLengthBetween(3, 10); + String id = randomAlphaOfLengthBetween(3, 10); + long version = randomNonNegativeLong(); + BasicHit hit = new BasicHit(index, id, version); + + hit.setRouting(randomAlphaOfLengthBetween(3, 20)); + hit.setSeqNo(randomNonNegativeLong()); + hit.setPrimaryTerm(randomNonNegativeLong()); + hit.setSource(new BytesArray(randomAlphaOfLengthBetween(5, 50)), randomFrom(XContentType.values())); + + assertEquals(index, hit.getIndex()); + assertEquals(id, hit.getId()); + assertEquals(version, hit.getVersion()); + } +} diff --git a/server/src/test/java/org/elasticsearch/index/reindex/ResponseTests.java b/server/src/test/java/org/elasticsearch/index/reindex/ResponseTests.java new file mode 100644 index 0000000000000..8a80dd6c6cc76 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/reindex/ResponseTests.java @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.reindex; + +import org.elasticsearch.index.reindex.PaginatedHitSource.BasicHit; +import org.elasticsearch.index.reindex.PaginatedHitSource.Hit; +import org.elasticsearch.index.reindex.PaginatedHitSource.Response; +import org.elasticsearch.index.reindex.PaginatedHitSource.SearchFailure; +import org.elasticsearch.test.ESTestCase; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ResponseTests extends ESTestCase { + + /** + * Verifies that all values provided to the constructor are returned unchanged by their respective getters. + */ + public void testConstructor() { + boolean timedOut = randomBoolean(); + List failures = randomBoolean() ? Collections.emptyList() : randomFailures(); + long totalHits = randomNonNegativeLong(); + List hits = randomBoolean() ? Collections.emptyList() : randomHits(); + String scrollId = randomAlphaOfLengthBetween(3, 20); + Response response = new Response(timedOut, failures, totalHits, hits, scrollId); + + assertEquals(timedOut, response.isTimedOut()); + assertSame(failures, response.getFailures()); + assertEquals(totalHits, response.getTotalHits()); + assertSame(hits, response.getHits()); + assertEquals(scrollId, response.getScrollId()); + } + + /** + * Verifies that providing null values for optional collections is preserved and returned as-is by the getters. + */ + public void testNullCollectionsArePreserved() { + List failures = null; + List hits = null; + Response response = new Response(randomBoolean(), failures, randomNonNegativeLong(), hits, randomAlphaOfLengthBetween(3, 20)); + assertNull(response.getFailures()); + assertNull(response.getHits()); + } + + private static List randomFailures() { + int size = randomIntBetween(1, 5); + List failures = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + failures.add( + new SearchFailure( + new IllegalStateException(randomAlphaOfLengthBetween(5, 20)), + randomBoolean() ? randomAlphaOfLengthBetween(3, 10) : null, + randomBoolean() ? randomIntBetween(0, 10) : null, + randomBoolean() ? randomAlphaOfLengthBetween(3, 10) : null + ) + ); + } + return failures; + } + + private static List randomHits() { + int size = randomIntBetween(1, 5); + List hits = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + hits.add(new BasicHit(randomAlphaOfLengthBetween(3, 10), randomAlphaOfLengthBetween(3, 10), randomNonNegativeLong())); + } + return hits; + } +} diff --git a/server/src/test/java/org/elasticsearch/index/reindex/SearchFailureTests.java b/server/src/test/java/org/elasticsearch/index/reindex/SearchFailureTests.java new file mode 100644 index 0000000000000..a65e0adc1d9a4 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/reindex/SearchFailureTests.java @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.reindex; + +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.index.reindex.PaginatedHitSource.SearchFailure; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xcontent.XContentType; + +import java.util.Map; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; + +public class SearchFailureTests extends ESTestCase { + + public void testConstructorWithReasonOnly() { + Throwable reason = randomException(); + SearchFailure failure = new SearchFailure(reason); + assertSame(reason, failure.getReason()); + assertNull(failure.getIndex()); + assertNull(failure.getShardId()); + assertNull(failure.getNodeId()); + assertEquals(ExceptionsHelper.status(reason), failure.getStatus()); + } + + public void testConstructorWithAllFields() { + Throwable reason = randomException(); + String index = randomAlphaOfLengthBetween(3, 10); + Integer shardId = randomIntBetween(0, 100); + String nodeId = randomAlphaOfLengthBetween(3, 10); + SearchFailure failure = new SearchFailure(reason, index, shardId, nodeId); + assertSame(reason, failure.getReason()); + assertEquals(index, failure.getIndex()); + assertEquals(shardId, failure.getShardId()); + assertEquals(nodeId, failure.getNodeId()); + assertEquals(ExceptionsHelper.status(reason), failure.getStatus()); + } + + public void testToXContentIncludesExpectedFields() { + String message = randomAlphaOfLengthBetween(1, 20); + Throwable reason = randomException(message); + String index = randomAlphaOfLengthBetween(3, 10); + Integer shardId = randomIntBetween(0, 10); + String nodeId = randomAlphaOfLengthBetween(3, 10); + SearchFailure failure = new SearchFailure(reason, index, shardId, nodeId); + String json = Strings.toString(failure); + Map map = XContentHelper.convertToMap(XContentType.JSON.xContent(), json, false); + assertThat(map.get(SearchFailure.INDEX_FIELD), equalTo(index)); + assertThat(map.get(SearchFailure.SHARD_FIELD), equalTo(shardId)); + assertThat(map.get(SearchFailure.NODE_FIELD), equalTo(nodeId)); + assertThat(map.get(SearchFailure.STATUS_FIELD), equalTo(failure.getStatus().getStatus())); + assertThat(map, hasKey(SearchFailure.REASON_FIELD)); + @SuppressWarnings("unchecked") + Map reasonMap = (Map) map.get(SearchFailure.REASON_FIELD); + assertThat(reasonMap.get("type"), notNullValue()); + assertThat(reasonMap.get("reason"), equalTo(message)); + } + + public void testToXContentOmitsNullOptionalFields() { + SearchFailure failure = new SearchFailure(randomException()); + String json = Strings.toString(failure); + Map map = XContentHelper.convertToMap(XContentType.JSON.xContent(), json, false); + assertThat(map, not(hasKey(SearchFailure.INDEX_FIELD))); + assertThat(map, not(hasKey(SearchFailure.SHARD_FIELD))); + assertThat(map, not(hasKey(SearchFailure.NODE_FIELD))); + assertThat(map, hasKey(SearchFailure.STATUS_FIELD)); + assertThat(map, hasKey(SearchFailure.REASON_FIELD)); + } + + public static Throwable randomException() { + return randomException(randomAlphaOfLengthBetween(1, 20)); + } + + public static Throwable randomException(String message) { + return randomFrom(new IllegalArgumentException(message), new IllegalStateException(message), new ElasticsearchException(message)); + } +} diff --git a/server/src/test/java/org/elasticsearch/index/reindex/SearchFailureWireSerialisationTests.java b/server/src/test/java/org/elasticsearch/index/reindex/SearchFailureWireSerialisationTests.java new file mode 100644 index 0000000000000..6c27e7f44e725 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/index/reindex/SearchFailureWireSerialisationTests.java @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +package org.elasticsearch.index.reindex; + +import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.index.reindex.PaginatedHitSource.SearchFailure; +import org.elasticsearch.test.AbstractWireSerializingTestCase; + +import java.io.IOException; +import java.util.Objects; + +import static org.elasticsearch.index.reindex.SearchFailureTests.randomException; + +public class SearchFailureWireSerialisationTests extends AbstractWireSerializingTestCase< + SearchFailureWireSerialisationTests.SearchFailureWrapper> { + @Override + protected SearchFailureWrapper createTestInstance() { + Throwable reason = randomException(); + String index = randomBoolean() ? randomAlphaOfLengthBetween(1, 10) : null; + Integer shardId = randomBoolean() ? randomIntBetween(0, 100) : null; + String nodeId = randomBoolean() ? randomAlphaOfLengthBetween(1, 10) : null; + return new SearchFailureWrapper(new SearchFailure(reason, index, shardId, nodeId)); + } + + @Override + protected Writeable.Reader instanceReader() { + return SearchFailureWrapper::new; + } + + @Override + protected SearchFailureWrapper mutateInstance(SearchFailureWrapper instance) { + return new SearchFailureWrapper(mutateSearchFailure(instance.failure())); + } + + /** + * Wrapper around {@link SearchFailure} used exclusively for wire-serialization tests. + *

+ * {@link AbstractWireSerializingTestCase} requires instances to be comparable via + * {@code equals}/{@code hashCode()}, but {@link SearchFailure} does not define + * suitable semantic equality due to its embedded {@link Throwable}. + *

+ * This wrapper provides stable, test-only equality semantics without leaking + * test concerns into production code. + */ + static final class SearchFailureWrapper implements Writeable { + private final SearchFailure failure; + + SearchFailureWrapper(SearchFailure failure) { + this.failure = failure; + } + + SearchFailureWrapper(StreamInput in) throws IOException { + this.failure = new SearchFailure(in); + } + + SearchFailure failure() { + return failure; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + failure.writeTo(out); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SearchFailureWrapper that = (SearchFailureWrapper) o; + return failuresEqual(failure, that.failure); + } + + @Override + public int hashCode() { + return Objects.hash( + failure.getIndex(), + failure.getShardId(), + failure.getNodeId(), + failure.getStatus(), + failure.getReason().getClass(), + failure.getReason().getMessage() + ); + } + + private static boolean failuresEqual(SearchFailure a, SearchFailure b) { + return Objects.equals(a.getIndex(), b.getIndex()) + && Objects.equals(a.getShardId(), b.getShardId()) + && Objects.equals(a.getNodeId(), b.getNodeId()) + && a.getStatus() == b.getStatus() + && a.getReason().getClass().equals(b.getReason().getClass()) + && Objects.equals(a.getReason().getMessage(), b.getReason().getMessage()); + } + } + + static SearchFailure mutateSearchFailure(SearchFailure instance) { + int fieldToMutate = randomIntBetween(0, 3); + return switch (fieldToMutate) { + case 0 -> { + Throwable newReason; + do { + newReason = randomException(); + } while (newReason.getClass().equals(instance.getReason().getClass()) + && Objects.equals(newReason.getMessage(), instance.getReason().getMessage())); + yield new SearchFailure( + newReason, + instance.getIndex(), + instance.getShardId(), + instance.getNodeId(), + ExceptionsHelper.status(newReason) + ); + } + case 1 -> { + String newIndex = instance.getIndex() == null + ? randomAlphaOfLengthBetween(1, 10) + : randomValueOtherThan(instance.getIndex(), () -> randomAlphaOfLengthBetween(1, 10)); + yield new SearchFailure(instance.getReason(), newIndex, instance.getShardId(), instance.getNodeId(), instance.getStatus()); + } + case 2 -> { + Integer newShardId = instance.getShardId() == null + ? randomIntBetween(0, 100) + : randomValueOtherThan(instance.getShardId(), () -> randomIntBetween(0, 100)); + yield new SearchFailure(instance.getReason(), instance.getIndex(), newShardId, instance.getNodeId(), instance.getStatus()); + } + case 3 -> { + String newNodeId = instance.getNodeId() == null + ? randomAlphaOfLengthBetween(1, 10) + : randomValueOtherThan(instance.getNodeId(), () -> randomAlphaOfLengthBetween(1, 10)); + yield new SearchFailure(instance.getReason(), instance.getIndex(), instance.getShardId(), newNodeId, instance.getStatus()); + } + default -> throw new AssertionError("Unknown field index [" + fieldToMutate + "]"); + }; + } +}