From 42a5852a437770d476fe658382bb9714a8878fa6 Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Tue, 7 Jul 2026 23:39:17 +0900 Subject: [PATCH 1/3] fix(action): forward dropped request params and fix wait_for_active_shards serialization Correctness fixes to existing HTTP actions, verified against the OpenSearch REST layer. No new dependencies and no API changes. - HttpOpenIndexAction: serialize wait_for_active_shards ActiveShardCount.ALL as "all" (was "ALL", which the server rejects with HTTP 400) and forward indices options. This was the remaining call site still using ActiveShardCount.toString(). - HttpCloseIndexAction: forward wait_for_active_shards and indices options (both were dropped, so close could return before shards were ready). - HttpUpdateAction: remove version/version_type params. The _update endpoint rejects internal versioning (it uses if_seq_no/if_primary_term, already sent), and UpdateRequest's version setters throw UnsupportedOperationException, so the removed block was unreachable. - HttpIndicesAliasesAction: build the request body via the request's own toXContent so is_write_index/is_hidden/must_exist/routing are emitted and a remove_index action no longer emits an empty "aliases". - HttpRefreshAction, HttpFlushAction, HttpForceMergeAction, HttpDeleteIndexAction, HttpGetMappingsAction: forward indices options (ignore_unavailable, allow_no_indices, expand_wildcards). - HttpForceMergeAction: also forward primary_only. Adds unit tests asserting the emitted query params / body for each fix. --- .../client/action/HttpCloseIndexAction.java | 5 ++ .../client/action/HttpDeleteIndexAction.java | 1 + .../fesen/client/action/HttpFlushAction.java | 1 + .../client/action/HttpForceMergeAction.java | 5 +- .../client/action/HttpGetMappingsAction.java | 1 + .../action/HttpIndicesAliasesAction.java | 37 ++++------- .../client/action/HttpOpenIndexAction.java | 3 +- .../client/action/HttpRefreshAction.java | 4 +- .../fesen/client/action/HttpUpdateAction.java | 8 --- .../action/HttpCloseIndexActionTest.java | 45 ++++++++++++++ .../action/HttpDeleteIndexActionTest.java | 40 ++++++++++++ .../client/action/HttpFlushActionTest.java | 43 +++++++++++++ .../action/HttpForceMergeActionTest.java | 57 +++++++++++++++++ .../action/HttpIndicesAliasesActionTest.java | 62 +++++++++++++++++++ .../action/HttpOpenIndexActionTest.java | 52 ++++++++++++++++ .../client/action/HttpRefreshActionTest.java | 40 ++++++++++++ .../client/action/HttpUpdateActionTest.java | 53 ++++++++++++++++ 17 files changed, 419 insertions(+), 38 deletions(-) create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpCloseIndexActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpDeleteIndexActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpFlushActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpForceMergeActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpIndicesAliasesActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpOpenIndexActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpRefreshActionTest.java create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpUpdateActionTest.java diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpCloseIndexAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpCloseIndexAction.java index 6a2a21b..21eaa2e 100644 --- a/src/main/java/org/codelibs/fesen/client/action/HttpCloseIndexAction.java +++ b/src/main/java/org/codelibs/fesen/client/action/HttpCloseIndexAction.java @@ -29,6 +29,7 @@ import org.opensearch.action.admin.indices.close.CloseIndexResponse.IndexResult; import org.opensearch.action.admin.indices.close.CloseIndexResponse.ShardResult; import org.opensearch.action.admin.indices.close.CloseIndexResponse.ShardResult.Failure; +import org.opensearch.action.support.ActiveShardCount; import org.opensearch.core.action.ActionListener; import org.opensearch.core.index.Index; import org.opensearch.core.xcontent.XContentParser; @@ -85,6 +86,10 @@ protected CurlRequest getCurlRequest(final CloseIndexRequest request) { if (request.masterNodeTimeout() != null) { curlRequest.param("master_timeout", request.masterNodeTimeout().toString()); } + if (!ActiveShardCount.DEFAULT.equals(request.waitForActiveShards())) { + curlRequest.param("wait_for_active_shards", getActiveShardsCountString(request.waitForActiveShards())); + } + appendIndicesOptions(curlRequest, request.indicesOptions()); return curlRequest; } diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpDeleteIndexAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpDeleteIndexAction.java index 58e8931..63e4d51 100644 --- a/src/main/java/org/codelibs/fesen/client/action/HttpDeleteIndexAction.java +++ b/src/main/java/org/codelibs/fesen/client/action/HttpDeleteIndexAction.java @@ -74,6 +74,7 @@ protected CurlRequest getCurlRequest(final DeleteIndexRequest request) { if (request.masterNodeTimeout() != null) { curlRequest.param("master_timeout", request.masterNodeTimeout().toString()); } + appendIndicesOptions(curlRequest, request.indicesOptions()); return curlRequest; } } diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpFlushAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpFlushAction.java index 40388dd..6dc94dd 100644 --- a/src/main/java/org/codelibs/fesen/client/action/HttpFlushAction.java +++ b/src/main/java/org/codelibs/fesen/client/action/HttpFlushAction.java @@ -70,6 +70,7 @@ protected CurlRequest getCurlRequest(final FlushRequest request) { final CurlRequest curlRequest = client.getCurlRequest(POST, "/_flush", request.indices()); curlRequest.param("wait_if_ongoing", Boolean.toString(request.waitIfOngoing())); curlRequest.param("force", Boolean.toString(request.force())); + appendIndicesOptions(curlRequest, request.indicesOptions()); return curlRequest; } } diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpForceMergeAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpForceMergeAction.java index 49dea06..388bb8d 100644 --- a/src/main/java/org/codelibs/fesen/client/action/HttpForceMergeAction.java +++ b/src/main/java/org/codelibs/fesen/client/action/HttpForceMergeAction.java @@ -68,8 +68,9 @@ public void execute(final ForceMergeRequest request, final ActionListener listener) { - String source = null; - try (final XContentBuilder builder = XContentFactory.jsonBuilder().startObject().startArray("actions")) { - for (final AliasActions aliasAction : request.getAliasActions()) { - builder.startObject().startObject(aliasAction.actionType().toString().toLowerCase()); - builder.array("indices", aliasAction.indices()); - builder.array("aliases", aliasAction.aliases()); - if (aliasAction.filter() != null) { - builder.field("filter", aliasAction.filter()); - } - if (aliasAction.indexRouting() != null) { - builder.field("index_routing", aliasAction.indexRouting()); - } - if (aliasAction.searchRouting() != null) { - builder.field("search_routing", aliasAction.searchRouting()); - } - builder.endObject().endObject(); - } - builder.endArray().endObject(); - builder.flush(); - source = BytesReference.bytes(builder).utf8ToString(); - } catch (final IOException e) { - throw new OpenSearchException("Failed to parse a request.", e); - } - getCurlRequest(request).body(source).execute(response -> { + getCurlRequest(request).execute(response -> { try (final XContentParser parser = createParser(response)) { final AcknowledgedResponse indicesAliasesResponse = AcknowledgedResponse.fromXContent(parser); listener.onResponse(indicesAliasesResponse); @@ -105,6 +82,14 @@ protected CurlRequest getCurlRequest(final IndicesAliasesRequest request) { if (request.masterNodeTimeout() != null) { curlRequest.param("master_timeout", request.masterNodeTimeout().toString()); } - return curlRequest; + String source = null; + try (final XContentBuilder builder = XContentFactory.jsonBuilder()) { + request.toXContent(builder, ToXContent.EMPTY_PARAMS); + builder.flush(); + source = BytesReference.bytes(builder).utf8ToString(); + } catch (final IOException e) { + throw new OpenSearchException("Failed to parse a request.", e); + } + return curlRequest.body(source); } } diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpOpenIndexAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpOpenIndexAction.java index 172072a..59a83f4 100644 --- a/src/main/java/org/codelibs/fesen/client/action/HttpOpenIndexAction.java +++ b/src/main/java/org/codelibs/fesen/client/action/HttpOpenIndexAction.java @@ -76,8 +76,9 @@ protected CurlRequest getCurlRequest(final OpenIndexRequest request) { curlRequest.param("master_timeout", request.masterNodeTimeout().toString()); } if (!ActiveShardCount.DEFAULT.equals(request.waitForActiveShards())) { - curlRequest.param("wait_for_active_shards", request.waitForActiveShards().toString()); + curlRequest.param("wait_for_active_shards", getActiveShardsCountString(request.waitForActiveShards())); } + appendIndicesOptions(curlRequest, request.indicesOptions()); return curlRequest; } } diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpRefreshAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpRefreshAction.java index 21d2b46..4c96d0a 100644 --- a/src/main/java/org/codelibs/fesen/client/action/HttpRefreshAction.java +++ b/src/main/java/org/codelibs/fesen/client/action/HttpRefreshAction.java @@ -66,6 +66,8 @@ public void execute(final RefreshRequest request, final ActionListener= 0) { - curlRequest.param("version", Long.toString(request.version())); - } - if (!VersionType.INTERNAL.equals(request.versionType())) { - curlRequest.param("version_type", request.versionType().name().toLowerCase(Locale.ROOT)); - } return curlRequest; } } diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpCloseIndexActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpCloseIndexActionTest.java new file mode 100644 index 0000000..b7a7f55 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpCloseIndexActionTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.close.CloseIndexAction; +import org.opensearch.action.admin.indices.close.CloseIndexRequest; +import org.opensearch.action.support.ActiveShardCount; + +class HttpCloseIndexActionTest { + + private final HttpCloseIndexAction clientAction = new HttpCloseIndexAction(ActionTestUtils.testClient(), CloseIndexAction.INSTANCE); + + @Test + void test_getCurlRequest_waitForActiveShardsAll() { + final CloseIndexRequest request = new CloseIndexRequest("test-index").waitForActiveShards(ActiveShardCount.ALL); + final Map params = ActionTestUtils.params(clientAction.getCurlRequest(request)); + assertEquals("all", params.get("wait_for_active_shards")); + } + + @Test + void test_getCurlRequest_waitForActiveShardsDefaultNotSent() { + final CloseIndexRequest request = new CloseIndexRequest("test-index").waitForActiveShards(ActiveShardCount.DEFAULT); + final Map params = ActionTestUtils.params(clientAction.getCurlRequest(request)); + assertFalse(params.containsKey("wait_for_active_shards")); + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpDeleteIndexActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpDeleteIndexActionTest.java new file mode 100644 index 0000000..d71b121 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpDeleteIndexActionTest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.delete.DeleteIndexAction; +import org.opensearch.action.admin.indices.delete.DeleteIndexRequest; +import org.opensearch.action.support.IndicesOptions; + +class HttpDeleteIndexActionTest { + + private final HttpDeleteIndexAction action = new HttpDeleteIndexAction(ActionTestUtils.testClient(), DeleteIndexAction.INSTANCE); + + @Test + void test_getCurlRequest_indicesOptions() { + final DeleteIndexRequest request = new DeleteIndexRequest("test-index"); + request.indicesOptions(IndicesOptions.fromOptions(true, false, false, true)); + final Map params = ActionTestUtils.params(action.getCurlRequest(request)); + assertEquals("true", params.get("ignore_unavailable")); + assertEquals("false", params.get("allow_no_indices")); + assertEquals("closed", params.get("expand_wildcards")); + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpFlushActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpFlushActionTest.java new file mode 100644 index 0000000..851e2fd --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpFlushActionTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.flush.FlushAction; +import org.opensearch.action.admin.indices.flush.FlushRequest; +import org.opensearch.action.support.IndicesOptions; + +class HttpFlushActionTest { + + private final HttpFlushAction action = new HttpFlushAction(ActionTestUtils.testClient(), FlushAction.INSTANCE); + + @Test + void test_getCurlRequest_indicesOptions() { + final FlushRequest request = new FlushRequest("test-index"); + request.indicesOptions(IndicesOptions.fromOptions(true, false, false, true)); + final Map params = ActionTestUtils.params(action.getCurlRequest(request)); + assertEquals("true", params.get("ignore_unavailable")); + assertEquals("false", params.get("allow_no_indices")); + assertEquals("closed", params.get("expand_wildcards")); + // existing flush params still present + assertEquals(String.valueOf(request.waitIfOngoing()), params.get("wait_if_ongoing")); + assertEquals(String.valueOf(request.force()), params.get("force")); + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpForceMergeActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpForceMergeActionTest.java new file mode 100644 index 0000000..9cf0ff8 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpForceMergeActionTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.forcemerge.ForceMergeAction; +import org.opensearch.action.admin.indices.forcemerge.ForceMergeRequest; +import org.opensearch.action.support.IndicesOptions; + +class HttpForceMergeActionTest { + + private final HttpForceMergeAction action = new HttpForceMergeAction(ActionTestUtils.testClient(), ForceMergeAction.INSTANCE); + + @Test + void test_getCurlRequest_primaryOnly() { + final ForceMergeRequest request = new ForceMergeRequest("test-index").primaryOnly(true); + final Map params = ActionTestUtils.params(action.getCurlRequest(request)); + assertEquals("true", params.get("primary_only")); + } + + @Test + void test_getCurlRequest_defaultParams() { + final ForceMergeRequest request = new ForceMergeRequest("test-index"); + final Map params = ActionTestUtils.params(action.getCurlRequest(request)); + assertEquals(String.valueOf(request.primaryOnly()), params.get("primary_only")); + assertEquals(String.valueOf(request.maxNumSegments()), params.get("max_num_segments")); + assertEquals(String.valueOf(request.onlyExpungeDeletes()), params.get("only_expunge_deletes")); + assertEquals(String.valueOf(request.flush()), params.get("flush")); + } + + @Test + void test_getCurlRequest_indicesOptions() { + final ForceMergeRequest request = new ForceMergeRequest("test-index"); + request.indicesOptions(IndicesOptions.fromOptions(true, false, false, true)); + final Map params = ActionTestUtils.params(action.getCurlRequest(request)); + assertEquals("true", params.get("ignore_unavailable")); + assertEquals("false", params.get("allow_no_indices")); + assertEquals("closed", params.get("expand_wildcards")); + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpIndicesAliasesActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpIndicesAliasesActionTest.java new file mode 100644 index 0000000..0da4e43 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpIndicesAliasesActionTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.alias.IndicesAliasesAction; +import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; + +class HttpIndicesAliasesActionTest { + + /** + * Builds the request body exactly as the production action does, by driving the + * real {@link HttpIndicesAliasesAction#getCurlRequest} and reading the emitted body. + */ + private static String toBody(final IndicesAliasesRequest request) { + final HttpIndicesAliasesAction action = new HttpIndicesAliasesAction(ActionTestUtils.testClient(), IndicesAliasesAction.INSTANCE); + return action.getCurlRequest(request).body(); + } + + @Test + void test_body_addAction_writeIndexAndHidden() { + final IndicesAliasesRequest request = new IndicesAliasesRequest(); + request.addAliasAction(AliasActions.add().index("test-index").alias("test-alias").writeIndex(true).isHidden(true)); + final String body = toBody(request); + assertTrue(body.contains("\"is_write_index\":true"), body); + assertTrue(body.contains("\"is_hidden\":true"), body); + } + + @Test + void test_body_addAction_routing() { + final IndicesAliasesRequest request = new IndicesAliasesRequest(); + request.addAliasAction(AliasActions.add().index("test-index").alias("test-alias").routing("r1")); + final String body = toBody(request); + assertTrue(body.contains("\"routing\":\"r1\""), body); + } + + @Test + void test_body_removeIndex_noBogusAliases() { + final IndicesAliasesRequest request = new IndicesAliasesRequest(); + request.addAliasAction(AliasActions.removeIndex().index("old-index")); + final String body = toBody(request); + assertTrue(body.contains("remove_index"), body); + assertFalse(body.contains("\"aliases\""), body); + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpOpenIndexActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpOpenIndexActionTest.java new file mode 100644 index 0000000..ec02508 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpOpenIndexActionTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.open.OpenIndexAction; +import org.opensearch.action.admin.indices.open.OpenIndexRequest; +import org.opensearch.action.support.ActiveShardCount; + +class HttpOpenIndexActionTest { + + private final HttpOpenIndexAction clientAction = new HttpOpenIndexAction(ActionTestUtils.testClient(), OpenIndexAction.INSTANCE); + + @Test + void test_getCurlRequest_waitForActiveShardsAll() { + final OpenIndexRequest request = new OpenIndexRequest("test-index").waitForActiveShards(ActiveShardCount.ALL); + final Map params = ActionTestUtils.params(clientAction.getCurlRequest(request)); + assertEquals("all", params.get("wait_for_active_shards")); + } + + @Test + void test_getCurlRequest_waitForActiveShardsDefaultNotSent() { + final OpenIndexRequest request = new OpenIndexRequest("test-index"); + final Map params = ActionTestUtils.params(clientAction.getCurlRequest(request)); + assertFalse(params.containsKey("wait_for_active_shards")); + } + + @Test + void test_getCurlRequest_waitForActiveShardsCount() { + final OpenIndexRequest request = new OpenIndexRequest("test-index").waitForActiveShards(ActiveShardCount.from(2)); + final Map params = ActionTestUtils.params(clientAction.getCurlRequest(request)); + assertEquals("2", params.get("wait_for_active_shards")); + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpRefreshActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpRefreshActionTest.java new file mode 100644 index 0000000..aab5b72 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpRefreshActionTest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.refresh.RefreshAction; +import org.opensearch.action.admin.indices.refresh.RefreshRequest; +import org.opensearch.action.support.IndicesOptions; + +class HttpRefreshActionTest { + + private final HttpRefreshAction action = new HttpRefreshAction(ActionTestUtils.testClient(), RefreshAction.INSTANCE); + + @Test + void test_getCurlRequest_indicesOptions() { + final RefreshRequest request = new RefreshRequest("test-index"); + request.indicesOptions(IndicesOptions.fromOptions(true, false, false, true)); + final Map params = ActionTestUtils.params(action.getCurlRequest(request)); + assertEquals("true", params.get("ignore_unavailable")); + assertEquals("false", params.get("allow_no_indices")); + assertEquals("closed", params.get("expand_wildcards")); + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpUpdateActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpUpdateActionTest.java new file mode 100644 index 0000000..13bbd24 --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpUpdateActionTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.opensearch.action.update.UpdateAction; +import org.opensearch.action.update.UpdateRequest; + +class HttpUpdateActionTest { + + private final HttpUpdateAction clientAction = new HttpUpdateAction(ActionTestUtils.testClient(), UpdateAction.INSTANCE); + + /** + * The {@code _update} REST endpoint (RestUpdateAction) rejects {@code version}/{@code version_type} + * with HTTP 400 because internal versioning is illegal for updates (UpdateRequest itself throws + * UnsupportedOperationException on its version/versionType setters and always reports MATCH_ANY / + * INTERNAL). Concurrency control must go through if_seq_no/if_primary_term instead. This action must + * therefore never emit version params. + */ + @Test + void test_getCurlRequest_versionParamsNotSent() { + final UpdateRequest request = new UpdateRequest("test-index", "1"); + final Map params = ActionTestUtils.params(clientAction.getCurlRequest(request)); + assertFalse(params.containsKey("version")); + assertFalse(params.containsKey("version_type")); + } + + @Test + void test_getCurlRequest_ifSeqNoAndPrimaryTerm() { + final UpdateRequest request = new UpdateRequest("test-index", "1").setIfSeqNo(5L).setIfPrimaryTerm(2L); + final Map params = ActionTestUtils.params(clientAction.getCurlRequest(request)); + assertEquals("5", params.get("if_seq_no")); + assertEquals("2", params.get("if_primary_term")); + } +} From 17a0824ba306221307cc5b095b3458f76a52bbad Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Wed, 8 Jul 2026 02:26:51 +0900 Subject: [PATCH 2/3] fix(action): send force-merge primary_only only on OpenSearch 2.x and later primary_only is unknown to OpenSearch 1.x and Elasticsearch 7/8, which reject unrecognized query parameters with HTTP 400 (the test_force_merge integration tests failed on those engines). Gate the parameter on the detected engine type, matching the version-conditional pattern already used by HttpGetMappingsAction. ActionTestUtils now stubs getEngineInfo() (with a per-engine-type overload) so version-conditional serialization can be asserted offline; HttpForceMergeActionTest covers both the OpenSearch 2.x/3.x (sent) and legacy (omitted) branches. --- .../client/action/HttpForceMergeAction.java | 14 +++- .../fesen/client/action/ActionTestUtils.java | 64 ++++++++++++++++--- .../action/HttpForceMergeActionTest.java | 26 ++++++++ 3 files changed, 91 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpForceMergeAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpForceMergeAction.java index 388bb8d..036f498 100644 --- a/src/main/java/org/codelibs/fesen/client/action/HttpForceMergeAction.java +++ b/src/main/java/org/codelibs/fesen/client/action/HttpForceMergeAction.java @@ -16,6 +16,7 @@ package org.codelibs.fesen.client.action; import org.codelibs.curl.CurlRequest; +import org.codelibs.fesen.client.EngineInfo.EngineType; import org.codelibs.fesen.client.HttpClient; import org.opensearch.action.admin.indices.forcemerge.ForceMergeAction; import org.opensearch.action.admin.indices.forcemerge.ForceMergeRequest; @@ -69,8 +70,15 @@ protected CurlRequest getCurlRequest(final ForceMergeRequest request) { // RestForceMergeAction final CurlRequest curlRequest = client.getCurlRequest(POST, "/_forcemerge", request.indices()); appendIndicesOptions(curlRequest, request.indicesOptions()); - return curlRequest.param("max_num_segments", String.valueOf(request.maxNumSegments())) - .param("only_expunge_deletes", String.valueOf(request.onlyExpungeDeletes())).param("flush", String.valueOf(request.flush())) - .param("primary_only", String.valueOf(request.primaryOnly())); + curlRequest.param("max_num_segments", String.valueOf(request.maxNumSegments())) + .param("only_expunge_deletes", String.valueOf(request.onlyExpungeDeletes())) + .param("flush", String.valueOf(request.flush())); + // primary_only is only recognized by OpenSearch 2.x and later; older OpenSearch and + // Elasticsearch reject unknown parameters with HTTP 400. + final EngineType engineType = client.getEngineInfo().getType(); + if (engineType == EngineType.OPENSEARCH2 || engineType == EngineType.OPENSEARCH3) { + curlRequest.param("primary_only", String.valueOf(request.primaryOnly())); + } + return curlRequest; } } diff --git a/src/test/java/org/codelibs/fesen/client/action/ActionTestUtils.java b/src/test/java/org/codelibs/fesen/client/action/ActionTestUtils.java index 0303b26..3fc72bc 100644 --- a/src/test/java/org/codelibs/fesen/client/action/ActionTestUtils.java +++ b/src/test/java/org/codelibs/fesen/client/action/ActionTestUtils.java @@ -22,9 +22,12 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import org.codelibs.curl.CurlRequest; +import org.codelibs.fesen.client.EngineInfo; +import org.codelibs.fesen.client.EngineInfo.EngineType; import org.codelibs.fesen.client.HttpClient; import org.codelibs.fesen.client.HttpClient.ContentType; import org.opensearch.common.settings.Settings; @@ -39,28 +42,35 @@ */ final class ActionTestUtils { - private static volatile HttpClient client; + private static final Map CLIENTS = new ConcurrentHashMap<>(); private ActionTestUtils() { } /** * Returns a shared {@link HttpClient} that builds plain, inspectable curl requests. + * The backend engine is reported as OpenSearch 3.x. * * @return the test HTTP client */ static HttpClient testClient() { - if (client == null) { - synchronized (ActionTestUtils.class) { - if (client == null) { - client = createClient(); - } - } - } - return client; + return testClient(EngineType.OPENSEARCH3); } - private static HttpClient createClient() { + /** + * Returns a shared {@link HttpClient} that builds plain, inspectable curl requests and + * reports the given backend engine type from {@link HttpClient#getEngineInfo()}, so that + * version-conditional parameter serialization can be exercised offline. + * + * @param engineType the backend engine type to report + * @return the test HTTP client + */ + static HttpClient testClient(final EngineType engineType) { + return CLIENTS.computeIfAbsent(engineType, ActionTestUtils::createClient); + } + + private static HttpClient createClient(final EngineType engineType) { + final EngineInfo engineInfo = stubEngineInfo(engineType); final Settings settings = Settings.builder().putList("http.hosts", "localhost:9200").build(); return new HttpClient(settings, null) { @Override @@ -75,9 +85,43 @@ public CurlRequest getCurlRequest(final Function method, fi } return method.apply(buf.toString()); } + + @Override + public EngineInfo getEngineInfo() { + return engineInfo; + } }; } + private static EngineInfo stubEngineInfo(final EngineType engineType) { + final String distribution; + final String number; + switch (engineType) { + case ELASTICSEARCH7: + distribution = "elasticsearch"; + number = "7.17.0"; + break; + case ELASTICSEARCH8: + distribution = "elasticsearch"; + number = "8.11.0"; + break; + case OPENSEARCH1: + distribution = "opensearch"; + number = "1.3.0"; + break; + case OPENSEARCH2: + distribution = "opensearch"; + number = "2.11.0"; + break; + case OPENSEARCH3: + default: + distribution = "opensearch"; + number = "3.0.0"; + break; + } + return new EngineInfo(Map.of("version", Map.of("number", number, "distribution", distribution))); + } + /** * Returns the raw {@code key=value} query parameter list of the given curl request, * read reflectively from the protected {@code paramList} field. diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpForceMergeActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpForceMergeActionTest.java index 9cf0ff8..ddcff5b 100644 --- a/src/test/java/org/codelibs/fesen/client/action/HttpForceMergeActionTest.java +++ b/src/test/java/org/codelibs/fesen/client/action/HttpForceMergeActionTest.java @@ -16,9 +16,11 @@ package org.codelibs.fesen.client.action; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import java.util.Map; +import org.codelibs.fesen.client.EngineInfo.EngineType; import org.junit.jupiter.api.Test; import org.opensearch.action.admin.indices.forcemerge.ForceMergeAction; import org.opensearch.action.admin.indices.forcemerge.ForceMergeRequest; @@ -35,6 +37,30 @@ void test_getCurlRequest_primaryOnly() { assertEquals("true", params.get("primary_only")); } + @Test + void test_getCurlRequest_primaryOnly_openSearch2() { + final HttpForceMergeAction os2Action = + new HttpForceMergeAction(ActionTestUtils.testClient(EngineType.OPENSEARCH2), ForceMergeAction.INSTANCE); + final ForceMergeRequest request = new ForceMergeRequest("test-index").primaryOnly(true); + final Map params = ActionTestUtils.params(os2Action.getCurlRequest(request)); + assertEquals("true", params.get("primary_only")); + } + + @Test + void test_getCurlRequest_primaryOnly_notSentOnLegacyEngines() { + // primary_only is unknown to OpenSearch 1.x and Elasticsearch 7/8, which reject it with HTTP 400. + for (final EngineType engineType : new EngineType[] { EngineType.OPENSEARCH1, EngineType.ELASTICSEARCH7, + EngineType.ELASTICSEARCH8 }) { + final HttpForceMergeAction legacyAction = + new HttpForceMergeAction(ActionTestUtils.testClient(engineType), ForceMergeAction.INSTANCE); + final ForceMergeRequest request = new ForceMergeRequest("test-index").primaryOnly(true); + final Map params = ActionTestUtils.params(legacyAction.getCurlRequest(request)); + assertFalse(params.containsKey("primary_only"), engineType.toString()); + // non-version-specific params are still forwarded + assertEquals(String.valueOf(request.maxNumSegments()), params.get("max_num_segments"), engineType.toString()); + } + } + @Test void test_getCurlRequest_defaultParams() { final ForceMergeRequest request = new ForceMergeRequest("test-index"); From f13e29f4bbdc07a01cee9bb9402f0c8cfac21de0 Mon Sep 17 00:00:00 2001 From: Shinsuke Sugaya Date: Wed, 8 Jul 2026 05:37:37 +0900 Subject: [PATCH 3/3] feat(pit): support Elasticsearch _pit with engine-aware gating; add integration and request-building test coverage Make the Point-In-Time actions engine-aware so they work on both OpenSearch 2.x+ and Elasticsearch 7.10+/8.x, and fail clearly where a backend has no equivalent: - CreatePit/DeletePit use POST/DELETE /_pit on Elasticsearch (mapping the {"id":...} and {"succeeded":...} bodies to the OpenSearch response types) and keep /_search/point_in_time on OpenSearch. - Unsupported combinations fail the listener with a clear UnsupportedOperationException: PIT on OpenSearch 1.x; GetAllPits and delete-all on Elasticsearch (no equivalent endpoint); and deleting multiple PIT ids in one request on Elasticsearch (the _pit endpoint accepts a single id, sent as {"id":"..."} rather than an array). - The Elasticsearch _pit endpoint rejects allow_partial_search_results, so it is not sent; the Elasticsearch response parsers now surface error bodies as failures instead of returning an empty/negative response (the HTTP layer routes non-2xx through the success path). Test coverage for the previously unguarded request-building surface: - Per-engine request-serialization unit tests for the PIT actions, and getCurlRequest param/body tests for reindex, update_by_query, delete_by_query, resolve_index and get_mappings. ActionTestUtils can now stub the reported engine type and read the request URL. - End-to-end integration tests across all five backends (OpenSearch 1/2/3, Elasticsearch 7/8) for reindex, update_by_query, delete_by_query, resolve_index and the PIT lifecycle, including the unsupported-engine assertions. --- .../client/action/HttpCreatePitAction.java | 100 +++++++++++- .../client/action/HttpDeletePitAction.java | 117 ++++++++++++-- .../client/action/HttpGetAllPitsAction.java | 20 ++- .../client/Elasticsearch7ClientTest.java | 133 +++++++++++++++ .../client/Elasticsearch8ClientTest.java | 133 +++++++++++++++ .../fesen/client/OpenSearch1ClientTest.java | 102 ++++++++++++ .../fesen/client/OpenSearch2ClientTest.java | 151 ++++++++++++++++++ .../fesen/client/OpenSearch3ClientTest.java | 151 ++++++++++++++++++ .../fesen/client/action/ActionTestUtils.java | 17 ++ .../action/HttpCreatePitActionTest.java | 106 ++++++++++++ .../action/HttpDeleteByQueryActionTest.java | 39 +++++ .../action/HttpDeletePitActionTest.java | 113 +++++++++++++ .../action/HttpGetAllPitsActionTest.java | 35 ++++ .../action/HttpGetMappingsActionTest.java | 97 +++++++++++ .../client/action/HttpReindexActionTest.java | 64 ++++++++ .../action/HttpResolveIndexActionTest.java | 18 +++ .../action/HttpUpdateByQueryActionTest.java | 42 +++++ 17 files changed, 1411 insertions(+), 27 deletions(-) create mode 100644 src/test/java/org/codelibs/fesen/client/action/HttpGetMappingsActionTest.java diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpCreatePitAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpCreatePitAction.java index f3b8bfb..8a1add9 100644 --- a/src/main/java/org/codelibs/fesen/client/action/HttpCreatePitAction.java +++ b/src/main/java/org/codelibs/fesen/client/action/HttpCreatePitAction.java @@ -16,12 +16,15 @@ package org.codelibs.fesen.client.action; import java.io.IOException; +import java.util.Map; import org.codelibs.curl.CurlRequest; +import org.codelibs.fesen.client.EngineInfo.EngineType; import org.codelibs.fesen.client.HttpClient; import org.opensearch.action.search.CreatePitAction; import org.opensearch.action.search.CreatePitRequest; import org.opensearch.action.search.CreatePitResponse; +import org.opensearch.action.search.ShardSearchFailure; import org.opensearch.action.support.IndicesOptions; import org.opensearch.core.action.ActionListener; import org.opensearch.core.xcontent.XContentParser; @@ -30,10 +33,18 @@ * Handles the Create Point-in-Time (PIT) API over HTTP, opening a PIT reader * context over one or more indices that can be reused across search requests. * - *

This action targets the OpenSearch REST API only. The - * {@code /{index}/_search/point_in_time} endpoint is OpenSearch-specific; - * Elasticsearch uses a different {@code _pit} endpoint and Elasticsearch 7 has - * no PIT support at all. No Elasticsearch adaptation is provided. + *

PIT is supported on OpenSearch 2.x and later via the + * {@code POST /{index}/_search/point_in_time} endpoint, and on Elasticsearch + * 7.10+ and 8.x via the {@code POST /{index}/_pit} endpoint. The two backends + * differ in both endpoint and query parameters (OpenSearch accepts + * {@code allow_partial_pit_creation}, which the Elasticsearch {@code _pit} + * endpoint rejects), and their response bodies differ, so the engine reported by + * {@link HttpClient#getEngineInfo()} selects the request and response handling. + * + *

PIT is not available on OpenSearch 1.x, and the engine cannot be + * determined for {@link EngineType#UNKNOWN}; in both cases + * {@link #execute(CreatePitRequest, ActionListener)} fails the listener with an + * {@link UnsupportedOperationException}. */ public class HttpCreatePitAction extends HttpAction { @@ -54,13 +65,23 @@ public HttpCreatePitAction(final HttpClient client, final CreatePitAction action /** * Executes the create PIT request and notifies the listener with the response. * + *

Fails the listener with an {@link UnsupportedOperationException} when the + * backend is OpenSearch 1.x or an unknown engine. + * * @param request the create PIT request containing the target indices and keep-alive * @param listener the listener notified with the response or a failure */ public void execute(final CreatePitRequest request, final ActionListener listener) { + final EngineType type = client.getEngineInfo().getType(); + if (type == EngineType.OPENSEARCH1 || type == EngineType.UNKNOWN) { + listener.onFailure(new UnsupportedOperationException( + "Point-In-Time is not supported on " + type + " over HTTP (requires OpenSearch 2.x+ or Elasticsearch 7.10+)")); + return; + } + final boolean elasticsearch = type == EngineType.ELASTICSEARCH7 || type == EngineType.ELASTICSEARCH8; getCurlRequest(request).execute(response -> { try (final XContentParser parser = createParser(response)) { - final CreatePitResponse pitResponse = fromXContent(parser); + final CreatePitResponse pitResponse = elasticsearch ? parseEsCreate(parser) : fromXContent(parser); listener.onResponse(pitResponse); } catch (final Exception e) { listener.onFailure(toOpenSearchException(response, e)); @@ -69,7 +90,7 @@ public void execute(final CreatePitRequest request, final ActionListener map = parser.map(); + final String id = (String) map.get("id"); + if (id == null) { + // Not a successful open-PIT body (e.g. an error response routed through the success + // path); surface it as a failure instead of returning a response with a null id. + throw new IOException("Unexpected create PIT response (missing \"id\"): " + map); + } + int total = 0; + int successful = 0; + int skipped = 0; + int failed = 0; + final Object shardsObj = map.get("_shards"); + if (shardsObj instanceof Map) { + @SuppressWarnings("unchecked") + final Map shards = (Map) shardsObj; + total = toInt(shards.get("total")); + successful = toInt(shards.get("successful")); + skipped = toInt(shards.get("skipped")); + failed = toInt(shards.get("failed")); + } + return new CreatePitResponse(id, System.currentTimeMillis(), total, successful, skipped, failed, ShardSearchFailure.EMPTY_ARRAY); + } + + private static int toInt(final Object value) { + return value instanceof Number ? ((Number) value).intValue() : 0; + } + + /** + * Builds the HTTP request for the create PIT API endpoint. The endpoint and query + * parameters differ between OpenSearch and Elasticsearch, selected by the engine + * reported by {@link HttpClient#getEngineInfo()}. * * @param request the create PIT request * @return the configured curl request */ protected CurlRequest getCurlRequest(final CreatePitRequest request) { - // RestCreatePitAction: POST /{index}/_search/point_in_time + final EngineType type = client.getEngineInfo().getType(); + if (type == EngineType.ELASTICSEARCH7 || type == EngineType.ELASTICSEARCH8) { + // Elasticsearch Open PIT API: POST /{index}/_pit + // Note: the Elasticsearch _pit endpoint does not accept allow_partial_pit_creation / + // allow_partial_search_results (unrecognized parameter -> HTTP 400), so it is not sent. + final CurlRequest curlRequest = client.getCurlRequest(POST, "/_pit", request.indices()); + if (request.getKeepAlive() != null) { + curlRequest.param("keep_alive", request.getKeepAlive().getStringRep()); + } + if (request.getPreference() != null) { + curlRequest.param("preference", request.getPreference()); + } + if (request.getRouting() != null) { + curlRequest.param("routing", request.getRouting()); + } + final IndicesOptions esIndicesOptions = request.indicesOptions(); + if (esIndicesOptions != null) { + appendIndicesOptions(curlRequest, esIndicesOptions); + } + return curlRequest; + } + // OpenSearch RestCreatePitAction: POST /{index}/_search/point_in_time final CurlRequest curlRequest = client.getCurlRequest(POST, "/_search/point_in_time", request.indices()); if (request.getKeepAlive() != null) { curlRequest.param("keep_alive", request.getKeepAlive().getStringRep()); diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpDeletePitAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpDeletePitAction.java index 59e210f..8eeb886 100644 --- a/src/main/java/org/codelibs/fesen/client/action/HttpDeletePitAction.java +++ b/src/main/java/org/codelibs/fesen/client/action/HttpDeletePitAction.java @@ -16,12 +16,16 @@ package org.codelibs.fesen.client.action; import java.io.IOException; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.codelibs.curl.CurlRequest; +import org.codelibs.fesen.client.EngineInfo.EngineType; import org.codelibs.fesen.client.HttpClient; import org.opensearch.OpenSearchException; import org.opensearch.action.search.DeletePitAction; +import org.opensearch.action.search.DeletePitInfo; import org.opensearch.action.search.DeletePitRequest; import org.opensearch.action.search.DeletePitResponse; import org.opensearch.common.xcontent.json.JsonXContent; @@ -35,15 +39,20 @@ * Handles the Delete Point-in-Time (PIT) API over HTTP, releasing one or more * PIT reader contexts. * - *

When the request targets the special {@code _all} identifier the delete-all - * endpoint {@code DELETE /_search/point_in_time/_all} is used with no body; - * otherwise the PIT identifiers are sent as a {@code {"pit_id":[...]}} body to - * {@code DELETE /_search/point_in_time}. + *

On OpenSearch 2.x+ a delete-by-id request sends a {@code {"pit_id":[...]}} + * body to {@code DELETE /_search/point_in_time}, and the special {@code _all} + * identifier targets {@code DELETE /_search/point_in_time/_all} with no body. On + * Elasticsearch 7.10+ and 8.x a delete-by-id request sends a {@code {"id":"..."}} + * body to {@code DELETE /_pit} (the endpoint accepts a single identifier only, so + * a request carrying multiple identifiers fails the listener with an + * {@link UnsupportedOperationException}); Elasticsearch has no delete-all + * endpoint, so a delete-all request likewise fails the listener with an + * {@link UnsupportedOperationException}. * - *

This action targets the OpenSearch REST API only. The - * {@code _search/point_in_time} endpoint is OpenSearch-specific; Elasticsearch - * uses a different {@code _pit} endpoint and Elasticsearch 7 has no PIT support - * at all. No Elasticsearch adaptation is provided. + *

PIT is not available on OpenSearch 1.x, and the engine cannot be determined + * for {@link EngineType#UNKNOWN}; in both cases + * {@link #execute(DeletePitRequest, ActionListener)} fails the listener with an + * {@link UnsupportedOperationException}. */ public class HttpDeletePitAction extends HttpAction { @@ -67,17 +76,39 @@ public HttpDeletePitAction(final HttpClient client, final DeletePitAction action /** * Executes the delete PIT request and notifies the listener with the response. * + *

Fails the listener with an {@link UnsupportedOperationException} when the + * backend is OpenSearch 1.x or an unknown engine, or when a delete-all request + * targets Elasticsearch (which has no delete-all endpoint). + * * @param request the delete PIT request containing the PIT identifiers to release * @param listener the listener notified with the response or a failure */ public void execute(final DeletePitRequest request, final ActionListener listener) { + final EngineType type = client.getEngineInfo().getType(); + if (type == EngineType.OPENSEARCH1 || type == EngineType.UNKNOWN) { + listener.onFailure(new UnsupportedOperationException( + "Point-In-Time is not supported on " + type + " over HTTP (requires OpenSearch 2.x+ or Elasticsearch 7.10+)")); + return; + } + final boolean elasticsearch = type == EngineType.ELASTICSEARCH7 || type == EngineType.ELASTICSEARCH8; + if (elasticsearch && isDeleteAll(request)) { + listener.onFailure(new UnsupportedOperationException("Delete-all PITs is not supported on Elasticsearch over HTTP")); + return; + } + if (elasticsearch && request.getPitIds() != null && request.getPitIds().size() > 1) { + // The Elasticsearch _pit endpoint accepts a single id per request (an array body is + // rejected with HTTP 400); callers must delete multiple PITs individually. + listener.onFailure(new UnsupportedOperationException( + "Deleting multiple PITs in a single request is not supported on Elasticsearch over HTTP; delete them individually")); + return; + } final CurlRequest curlRequest = getCurlRequest(request); if (!isDeleteAll(request)) { - curlRequest.body(buildBody(request)); + curlRequest.body(elasticsearch ? buildEsBody(request) : buildBody(request)); } curlRequest.execute(response -> { try (final XContentParser parser = createParser(response)) { - final DeletePitResponse pitResponse = fromXContent(parser); + final DeletePitResponse pitResponse = elasticsearch ? parseEsDelete(parser, request) : fromXContent(parser); listener.onResponse(pitResponse); } catch (final Exception e) { listener.onFailure(toOpenSearchException(response, e)); @@ -86,7 +117,7 @@ public void execute(final DeletePitRequest request, final ActionListener map = parser.map(); + if (!map.containsKey("succeeded")) { + // Not a successful close-PIT body (e.g. an error response routed through the success + // path); surface it as a failure instead of reporting every id as failed. + throw new IOException("Unexpected delete PIT response (missing \"succeeded\"): " + map); + } + final boolean succeeded = Boolean.TRUE.equals(map.get("succeeded")); + final List results = new ArrayList<>(); + for (final String pitId : request.getPitIds()) { + results.add(new DeletePitInfo(succeeded, pitId)); + } + return new DeletePitResponse(results); + } + + /** + * Builds the HTTP request for the delete PIT API endpoint. The endpoint is selected by the + * engine reported by {@link HttpClient#getEngineInfo()}: Elasticsearch uses {@code /_pit}, + * OpenSearch uses {@code /_search/point_in_time} (or its {@code /_all} variant for a + * delete-all request). * * @param request the delete PIT request * @return the configured curl request */ protected CurlRequest getCurlRequest(final DeletePitRequest request) { - // RestDeletePitAction + final EngineType type = client.getEngineInfo().getType(); + if (type == EngineType.ELASTICSEARCH7 || type == EngineType.ELASTICSEARCH8) { + // Elasticsearch Close PIT API: DELETE /_pit + return client.getCurlRequest(DELETE, "/_pit"); + } + // OpenSearch RestDeletePitAction if (isDeleteAll(request)) { return client.getCurlRequest(DELETE, "/_search/point_in_time/_all"); } @@ -124,7 +190,8 @@ protected static boolean isDeleteAll(final DeletePitRequest request) { } /** - * Serializes the delete PIT request body as a {@code {"pit_id":[...]}} JSON object. + * Serializes the delete PIT request body as a {@code {"pit_id":[...]}} JSON object + * for OpenSearch. * * @param request the delete PIT request * @return the JSON request body @@ -138,4 +205,24 @@ protected String buildBody(final DeletePitRequest request) { throw new OpenSearchException("Failed to build a request.", e); } } + + /** + * Serializes the delete PIT request body as a {@code {"id":"..."}} JSON object for + * Elasticsearch, which accepts a single identifier only. Callers with multiple identifiers + * are rejected earlier in {@link #execute(DeletePitRequest, ActionListener)}. + * + * @param request the delete PIT request + * @return the JSON request body + */ + protected String buildEsBody(final DeletePitRequest request) { + try (final XContentBuilder builder = JsonXContent.contentBuilder()) { + builder.startObject(); + builder.field("id", request.getPitIds().get(0)); + builder.endObject(); + builder.flush(); + return BytesReference.bytes(builder).utf8ToString(); + } catch (final IOException e) { + throw new OpenSearchException("Failed to build a request.", e); + } + } } diff --git a/src/main/java/org/codelibs/fesen/client/action/HttpGetAllPitsAction.java b/src/main/java/org/codelibs/fesen/client/action/HttpGetAllPitsAction.java index d4520b6..deae090 100644 --- a/src/main/java/org/codelibs/fesen/client/action/HttpGetAllPitsAction.java +++ b/src/main/java/org/codelibs/fesen/client/action/HttpGetAllPitsAction.java @@ -18,6 +18,7 @@ import java.io.IOException; import org.codelibs.curl.CurlRequest; +import org.codelibs.fesen.client.EngineInfo.EngineType; import org.codelibs.fesen.client.HttpClient; import org.opensearch.action.search.GetAllPitNodesRequest; import org.opensearch.action.search.GetAllPitNodesResponse; @@ -29,10 +30,12 @@ * Handles the Get All Point-in-Time (PIT) API over HTTP, listing every active * PIT reader context across the cluster. * - *

This action targets the OpenSearch REST API only. The - * {@code /_search/point_in_time/_all} endpoint is OpenSearch-specific; - * Elasticsearch uses a different {@code _pit} endpoint and Elasticsearch 7 has - * no PIT support at all. No Elasticsearch adaptation is provided. + *

This API is OpenSearch-specific and supported only on OpenSearch 2.x and + * later via the {@code GET /_search/point_in_time/_all} endpoint. Elasticsearch + * (7.x and 8.x) has no equivalent list-all endpoint, OpenSearch 1.x predates + * PIT, and the engine cannot be determined for {@link EngineType#UNKNOWN}; + * in all of these cases {@link #execute(GetAllPitNodesRequest, ActionListener)} + * fails the listener with an {@link UnsupportedOperationException}. */ public class HttpGetAllPitsAction extends HttpAction { @@ -53,10 +56,19 @@ public HttpGetAllPitsAction(final HttpClient client, final GetAllPitsAction acti /** * Executes the get all PITs request and notifies the listener with the response. * + *

Fails the listener with an {@link UnsupportedOperationException} on any backend + * other than OpenSearch 2.x+, since listing all PITs is OpenSearch-specific. + * * @param request the get all PITs request * @param listener the listener notified with the response or a failure */ public void execute(final GetAllPitNodesRequest request, final ActionListener listener) { + final EngineType type = client.getEngineInfo().getType(); + if (type != EngineType.OPENSEARCH2 && type != EngineType.OPENSEARCH3) { + listener.onFailure(new UnsupportedOperationException( + "Listing all PITs is not supported on " + type + " over HTTP (OpenSearch 2.x+ only)")); + return; + } getCurlRequest(request).execute(response -> { try (final XContentParser parser = createParser(response)) { final GetAllPitNodesResponse pitResponse = fromXContent(parser); diff --git a/src/test/java/org/codelibs/fesen/client/Elasticsearch7ClientTest.java b/src/test/java/org/codelibs/fesen/client/Elasticsearch7ClientTest.java index 45fd0ee..a396568 100644 --- a/src/test/java/org/codelibs/fesen/client/Elasticsearch7ClientTest.java +++ b/src/test/java/org/codelibs/fesen/client/Elasticsearch7ClientTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.opensearch.core.action.ActionListener.wrap; @@ -1298,4 +1299,136 @@ void test_info() throws Exception { assertEquals("fesen", mainResponse.getClusterName().value()); } } + + @Test + void test_reindex() throws Exception { + final String srcIndex = "test_reindex_src"; + final String destIndex = "test_reindex_dest"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 3; i++) { + bulkRequestBuilder.add(client.prepareIndex().setIndex(srcIndex).setId(String.valueOf(i)).setSource("{\"value\":" + i + "}", + XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(srcIndex).execute().actionGet(); + + final org.opensearch.index.reindex.ReindexRequest reindexRequest = new org.opensearch.index.reindex.ReindexRequest(); + reindexRequest.setSourceIndices(srcIndex); + reindexRequest.setDestIndex(destIndex); + reindexRequest.setRefresh(true); + final org.opensearch.index.reindex.BulkByScrollResponse response = + client.execute(org.opensearch.index.reindex.ReindexAction.INSTANCE, reindexRequest).actionGet(); + assertEquals(3L, response.getTotal()); + assertEquals(3L, response.getCreated()); + assertEquals(0, response.getBulkFailures().size()); + + client.admin().indices().prepareRefresh(destIndex).execute().actionGet(); + final SearchResponse searchResponse = client.prepareSearch(destIndex).setQuery(QueryBuilders.matchAllQuery()).execute().actionGet(); + assertEquals(3L, searchResponse.getHits().getTotalHits().value()); + } + + @Test + void test_update_by_query() throws Exception { + final String index = "test_update_by_query"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 3; i++) { + bulkRequestBuilder.add(client.prepareIndex().setIndex(index).setId(String.valueOf(i)) + .setSource("{\"value\":" + i + ",\"status\":\"new\"}", XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(index).execute().actionGet(); + + final org.opensearch.index.reindex.UpdateByQueryRequest request = new org.opensearch.index.reindex.UpdateByQueryRequest(index); + request.setQuery(QueryBuilders.matchAllQuery()); + request.setScript(new org.opensearch.script.Script("ctx._source.status = 'updated'")); + request.setConflicts("proceed"); + request.setRefresh(true); + final org.opensearch.index.reindex.BulkByScrollResponse response = + client.execute(org.opensearch.index.reindex.UpdateByQueryAction.INSTANCE, request).actionGet(); + assertEquals(3L, response.getTotal()); + assertEquals(3L, response.getUpdated()); + assertEquals(0, response.getBulkFailures().size()); + + final SearchResponse searchResponse = + client.prepareSearch(index).setQuery(QueryBuilders.matchQuery("status", "updated")).execute().actionGet(); + assertEquals(3L, searchResponse.getHits().getTotalHits().value()); + } + + @Test + void test_delete_by_query() throws Exception { + final String index = "test_delete_by_query"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 5; i++) { + final String group = i <= 2 ? "a" : "b"; + bulkRequestBuilder.add(client.prepareIndex().setIndex(index).setId(String.valueOf(i)) + .setSource("{\"value\":" + i + ",\"group\":\"" + group + "\"}", XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(index).execute().actionGet(); + + final org.opensearch.index.reindex.DeleteByQueryRequest request = new org.opensearch.index.reindex.DeleteByQueryRequest(index); + request.setQuery(QueryBuilders.matchQuery("group", "a")); + request.setConflicts("proceed"); + request.setRefresh(true); + final org.opensearch.index.reindex.BulkByScrollResponse response = + client.execute(org.opensearch.index.reindex.DeleteByQueryAction.INSTANCE, request).actionGet(); + assertEquals(2L, response.getDeleted()); + assertEquals(0, response.getBulkFailures().size()); + + final SearchResponse searchResponse = client.prepareSearch(index).setQuery(QueryBuilders.matchAllQuery()).execute().actionGet(); + assertEquals(3L, searchResponse.getHits().getTotalHits().value()); + } + + @Test + void test_resolve_index() throws Exception { + final String index = "test_resolve_index"; + final String alias = "test_resolve_index_alias"; + client.admin().indices().prepareCreate(index).addAlias(new Alias(alias)).execute().actionGet(); + + final org.opensearch.action.admin.indices.resolve.ResolveIndexAction.Request request = + new org.opensearch.action.admin.indices.resolve.ResolveIndexAction.Request(new String[] { "test_resolve_index*" }); + final org.opensearch.action.admin.indices.resolve.ResolveIndexAction.Response response = + client.execute(org.opensearch.action.admin.indices.resolve.ResolveIndexAction.INSTANCE, request).actionGet(); + assertTrue(response.getIndices().stream().anyMatch(i -> index.equals(i.getName()))); + assertTrue(response.getAliases().stream().anyMatch(a -> alias.equals(a.getName()))); + } + + @Test + void test_pit_create_delete() throws Exception { + final String index = "test_pit_create_delete"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 3; i++) { + bulkRequestBuilder.add( + client.prepareIndex().setIndex(index).setId(String.valueOf(i)).setSource("{\"value\":" + i + "}", XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(index).execute().actionGet(); + + final org.opensearch.action.search.CreatePitRequest createPitRequest = + new org.opensearch.action.search.CreatePitRequest(TimeValue.timeValueMinutes(1), true, index); + final org.opensearch.action.search.CreatePitResponse createPitResponse = + client.execute(org.opensearch.action.search.CreatePitAction.INSTANCE, createPitRequest).actionGet(); + final String pitId = createPitResponse.getId(); + assertNotNull(pitId); + assertFalse(pitId.isEmpty()); + + final org.opensearch.action.search.DeletePitResponse deletePitResponse = client + .execute(org.opensearch.action.search.DeletePitAction.INSTANCE, new org.opensearch.action.search.DeletePitRequest(pitId)) + .actionGet(); + assertTrue(deletePitResponse.getDeletePitResults().stream().allMatch(org.opensearch.action.search.DeletePitInfo::isSuccessful)); + } + + @Test + void test_pit_get_all_unsupported() throws Exception { + assertThrows(UnsupportedOperationException.class, () -> client + .execute(org.opensearch.action.search.GetAllPitsAction.INSTANCE, new org.opensearch.action.search.GetAllPitNodesRequest()) + .actionGet()); + } + + @Test + void test_pit_delete_all_unsupported() throws Exception { + assertThrows(UnsupportedOperationException.class, () -> client + .execute(org.opensearch.action.search.DeletePitAction.INSTANCE, new org.opensearch.action.search.DeletePitRequest("_all")) + .actionGet()); + } } diff --git a/src/test/java/org/codelibs/fesen/client/Elasticsearch8ClientTest.java b/src/test/java/org/codelibs/fesen/client/Elasticsearch8ClientTest.java index 3426179..0d83239 100644 --- a/src/test/java/org/codelibs/fesen/client/Elasticsearch8ClientTest.java +++ b/src/test/java/org/codelibs/fesen/client/Elasticsearch8ClientTest.java @@ -18,6 +18,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.opensearch.core.action.ActionListener.wrap; @@ -1303,4 +1304,136 @@ void test_info() throws Exception { assertEquals("fesen", mainResponse.getClusterName().value()); } } + + @Test + void test_reindex() throws Exception { + final String srcIndex = "test_reindex_src"; + final String destIndex = "test_reindex_dest"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 3; i++) { + bulkRequestBuilder.add(client.prepareIndex().setIndex(srcIndex).setId(String.valueOf(i)).setSource("{\"value\":" + i + "}", + XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(srcIndex).execute().actionGet(); + + final org.opensearch.index.reindex.ReindexRequest reindexRequest = new org.opensearch.index.reindex.ReindexRequest(); + reindexRequest.setSourceIndices(srcIndex); + reindexRequest.setDestIndex(destIndex); + reindexRequest.setRefresh(true); + final org.opensearch.index.reindex.BulkByScrollResponse response = + client.execute(org.opensearch.index.reindex.ReindexAction.INSTANCE, reindexRequest).actionGet(); + assertEquals(3L, response.getTotal()); + assertEquals(3L, response.getCreated()); + assertEquals(0, response.getBulkFailures().size()); + + client.admin().indices().prepareRefresh(destIndex).execute().actionGet(); + final SearchResponse searchResponse = client.prepareSearch(destIndex).setQuery(QueryBuilders.matchAllQuery()).execute().actionGet(); + assertEquals(3L, searchResponse.getHits().getTotalHits().value()); + } + + @Test + void test_update_by_query() throws Exception { + final String index = "test_update_by_query"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 3; i++) { + bulkRequestBuilder.add(client.prepareIndex().setIndex(index).setId(String.valueOf(i)) + .setSource("{\"value\":" + i + ",\"status\":\"new\"}", XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(index).execute().actionGet(); + + final org.opensearch.index.reindex.UpdateByQueryRequest request = new org.opensearch.index.reindex.UpdateByQueryRequest(index); + request.setQuery(QueryBuilders.matchAllQuery()); + request.setScript(new org.opensearch.script.Script("ctx._source.status = 'updated'")); + request.setConflicts("proceed"); + request.setRefresh(true); + final org.opensearch.index.reindex.BulkByScrollResponse response = + client.execute(org.opensearch.index.reindex.UpdateByQueryAction.INSTANCE, request).actionGet(); + assertEquals(3L, response.getTotal()); + assertEquals(3L, response.getUpdated()); + assertEquals(0, response.getBulkFailures().size()); + + final SearchResponse searchResponse = + client.prepareSearch(index).setQuery(QueryBuilders.matchQuery("status", "updated")).execute().actionGet(); + assertEquals(3L, searchResponse.getHits().getTotalHits().value()); + } + + @Test + void test_delete_by_query() throws Exception { + final String index = "test_delete_by_query"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 5; i++) { + final String group = i <= 2 ? "a" : "b"; + bulkRequestBuilder.add(client.prepareIndex().setIndex(index).setId(String.valueOf(i)) + .setSource("{\"value\":" + i + ",\"group\":\"" + group + "\"}", XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(index).execute().actionGet(); + + final org.opensearch.index.reindex.DeleteByQueryRequest request = new org.opensearch.index.reindex.DeleteByQueryRequest(index); + request.setQuery(QueryBuilders.matchQuery("group", "a")); + request.setConflicts("proceed"); + request.setRefresh(true); + final org.opensearch.index.reindex.BulkByScrollResponse response = + client.execute(org.opensearch.index.reindex.DeleteByQueryAction.INSTANCE, request).actionGet(); + assertEquals(2L, response.getDeleted()); + assertEquals(0, response.getBulkFailures().size()); + + final SearchResponse searchResponse = client.prepareSearch(index).setQuery(QueryBuilders.matchAllQuery()).execute().actionGet(); + assertEquals(3L, searchResponse.getHits().getTotalHits().value()); + } + + @Test + void test_resolve_index() throws Exception { + final String index = "test_resolve_index"; + final String alias = "test_resolve_index_alias"; + client.admin().indices().prepareCreate(index).addAlias(new Alias(alias)).execute().actionGet(); + + final org.opensearch.action.admin.indices.resolve.ResolveIndexAction.Request request = + new org.opensearch.action.admin.indices.resolve.ResolveIndexAction.Request(new String[] { "test_resolve_index*" }); + final org.opensearch.action.admin.indices.resolve.ResolveIndexAction.Response response = + client.execute(org.opensearch.action.admin.indices.resolve.ResolveIndexAction.INSTANCE, request).actionGet(); + assertTrue(response.getIndices().stream().anyMatch(i -> index.equals(i.getName()))); + assertTrue(response.getAliases().stream().anyMatch(a -> alias.equals(a.getName()))); + } + + @Test + void test_pit_create_delete() throws Exception { + final String index = "test_pit_create_delete"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 3; i++) { + bulkRequestBuilder.add( + client.prepareIndex().setIndex(index).setId(String.valueOf(i)).setSource("{\"value\":" + i + "}", XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(index).execute().actionGet(); + + final org.opensearch.action.search.CreatePitRequest createPitRequest = + new org.opensearch.action.search.CreatePitRequest(TimeValue.timeValueMinutes(1), true, index); + final org.opensearch.action.search.CreatePitResponse createPitResponse = + client.execute(org.opensearch.action.search.CreatePitAction.INSTANCE, createPitRequest).actionGet(); + final String pitId = createPitResponse.getId(); + assertNotNull(pitId); + assertFalse(pitId.isEmpty()); + + final org.opensearch.action.search.DeletePitResponse deletePitResponse = client + .execute(org.opensearch.action.search.DeletePitAction.INSTANCE, new org.opensearch.action.search.DeletePitRequest(pitId)) + .actionGet(); + assertTrue(deletePitResponse.getDeletePitResults().stream().allMatch(org.opensearch.action.search.DeletePitInfo::isSuccessful)); + } + + @Test + void test_pit_get_all_unsupported() throws Exception { + assertThrows(UnsupportedOperationException.class, () -> client + .execute(org.opensearch.action.search.GetAllPitsAction.INSTANCE, new org.opensearch.action.search.GetAllPitNodesRequest()) + .actionGet()); + } + + @Test + void test_pit_delete_all_unsupported() throws Exception { + assertThrows(UnsupportedOperationException.class, () -> client + .execute(org.opensearch.action.search.DeletePitAction.INSTANCE, new org.opensearch.action.search.DeletePitRequest("_all")) + .actionGet()); + } } diff --git a/src/test/java/org/codelibs/fesen/client/OpenSearch1ClientTest.java b/src/test/java/org/codelibs/fesen/client/OpenSearch1ClientTest.java index f83d976..040aed1 100644 --- a/src/test/java/org/codelibs/fesen/client/OpenSearch1ClientTest.java +++ b/src/test/java/org/codelibs/fesen/client/OpenSearch1ClientTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.opensearch.core.action.ActionListener.wrap; @@ -1299,4 +1300,105 @@ void test_info() throws Exception { assertEquals("fesen", mainResponse.getClusterName().value()); } } + + @Test + void test_reindex() throws Exception { + final String srcIndex = "test_reindex_src"; + final String destIndex = "test_reindex_dest"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 3; i++) { + bulkRequestBuilder.add(client.prepareIndex().setIndex(srcIndex).setId(String.valueOf(i)).setSource("{\"value\":" + i + "}", + XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(srcIndex).execute().actionGet(); + + final org.opensearch.index.reindex.ReindexRequest reindexRequest = new org.opensearch.index.reindex.ReindexRequest(); + reindexRequest.setSourceIndices(srcIndex); + reindexRequest.setDestIndex(destIndex); + reindexRequest.setRefresh(true); + final org.opensearch.index.reindex.BulkByScrollResponse response = + client.execute(org.opensearch.index.reindex.ReindexAction.INSTANCE, reindexRequest).actionGet(); + assertEquals(3L, response.getTotal()); + assertEquals(3L, response.getCreated()); + assertEquals(0, response.getBulkFailures().size()); + + client.admin().indices().prepareRefresh(destIndex).execute().actionGet(); + final SearchResponse searchResponse = client.prepareSearch(destIndex).setQuery(QueryBuilders.matchAllQuery()).execute().actionGet(); + assertEquals(3L, searchResponse.getHits().getTotalHits().value()); + } + + @Test + void test_update_by_query() throws Exception { + final String index = "test_update_by_query"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 3; i++) { + bulkRequestBuilder.add(client.prepareIndex().setIndex(index).setId(String.valueOf(i)) + .setSource("{\"value\":" + i + ",\"status\":\"new\"}", XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(index).execute().actionGet(); + + final org.opensearch.index.reindex.UpdateByQueryRequest request = new org.opensearch.index.reindex.UpdateByQueryRequest(index); + request.setQuery(QueryBuilders.matchAllQuery()); + request.setScript(new org.opensearch.script.Script("ctx._source.status = 'updated'")); + request.setConflicts("proceed"); + request.setRefresh(true); + final org.opensearch.index.reindex.BulkByScrollResponse response = + client.execute(org.opensearch.index.reindex.UpdateByQueryAction.INSTANCE, request).actionGet(); + assertEquals(3L, response.getTotal()); + assertEquals(3L, response.getUpdated()); + assertEquals(0, response.getBulkFailures().size()); + + final SearchResponse searchResponse = + client.prepareSearch(index).setQuery(QueryBuilders.matchQuery("status", "updated")).execute().actionGet(); + assertEquals(3L, searchResponse.getHits().getTotalHits().value()); + } + + @Test + void test_delete_by_query() throws Exception { + final String index = "test_delete_by_query"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 5; i++) { + final String group = i <= 2 ? "a" : "b"; + bulkRequestBuilder.add(client.prepareIndex().setIndex(index).setId(String.valueOf(i)) + .setSource("{\"value\":" + i + ",\"group\":\"" + group + "\"}", XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(index).execute().actionGet(); + + final org.opensearch.index.reindex.DeleteByQueryRequest request = new org.opensearch.index.reindex.DeleteByQueryRequest(index); + request.setQuery(QueryBuilders.matchQuery("group", "a")); + request.setConflicts("proceed"); + request.setRefresh(true); + final org.opensearch.index.reindex.BulkByScrollResponse response = + client.execute(org.opensearch.index.reindex.DeleteByQueryAction.INSTANCE, request).actionGet(); + assertEquals(2L, response.getDeleted()); + assertEquals(0, response.getBulkFailures().size()); + + final SearchResponse searchResponse = client.prepareSearch(index).setQuery(QueryBuilders.matchAllQuery()).execute().actionGet(); + assertEquals(3L, searchResponse.getHits().getTotalHits().value()); + } + + @Test + void test_resolve_index() throws Exception { + final String index = "test_resolve_index"; + final String alias = "test_resolve_index_alias"; + client.admin().indices().prepareCreate(index).addAlias(new Alias(alias)).execute().actionGet(); + + final org.opensearch.action.admin.indices.resolve.ResolveIndexAction.Request request = + new org.opensearch.action.admin.indices.resolve.ResolveIndexAction.Request(new String[] { "test_resolve_index*" }); + final org.opensearch.action.admin.indices.resolve.ResolveIndexAction.Response response = + client.execute(org.opensearch.action.admin.indices.resolve.ResolveIndexAction.INSTANCE, request).actionGet(); + assertTrue(response.getIndices().stream().anyMatch(i -> index.equals(i.getName()))); + assertTrue(response.getAliases().stream().anyMatch(a -> alias.equals(a.getName()))); + } + + @Test + void test_pit_unsupported() throws Exception { + final org.opensearch.action.search.CreatePitRequest createPitRequest = + new org.opensearch.action.search.CreatePitRequest(TimeValue.timeValueMinutes(1), true, "test_pit_unsupported"); + assertThrows(UnsupportedOperationException.class, + () -> client.execute(org.opensearch.action.search.CreatePitAction.INSTANCE, createPitRequest).actionGet()); + } } diff --git a/src/test/java/org/codelibs/fesen/client/OpenSearch2ClientTest.java b/src/test/java/org/codelibs/fesen/client/OpenSearch2ClientTest.java index 5ac1b16..4ab720a 100644 --- a/src/test/java/org/codelibs/fesen/client/OpenSearch2ClientTest.java +++ b/src/test/java/org/codelibs/fesen/client/OpenSearch2ClientTest.java @@ -1371,4 +1371,155 @@ void test_info() throws Exception { assertEquals("fesen", mainResponse.getClusterName().value()); } } + + @Test + void test_reindex() throws Exception { + final String srcIndex = "test_reindex_src"; + final String destIndex = "test_reindex_dest"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 3; i++) { + bulkRequestBuilder.add(client.prepareIndex().setIndex(srcIndex).setId(String.valueOf(i)).setSource("{\"value\":" + i + "}", + XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(srcIndex).execute().actionGet(); + + final org.opensearch.index.reindex.ReindexRequest reindexRequest = new org.opensearch.index.reindex.ReindexRequest(); + reindexRequest.setSourceIndices(srcIndex); + reindexRequest.setDestIndex(destIndex); + reindexRequest.setRefresh(true); + final org.opensearch.index.reindex.BulkByScrollResponse response = + client.execute(org.opensearch.index.reindex.ReindexAction.INSTANCE, reindexRequest).actionGet(); + assertEquals(3L, response.getTotal()); + assertEquals(3L, response.getCreated()); + assertEquals(0, response.getBulkFailures().size()); + + client.admin().indices().prepareRefresh(destIndex).execute().actionGet(); + final SearchResponse searchResponse = client.prepareSearch(destIndex).setQuery(QueryBuilders.matchAllQuery()).execute().actionGet(); + assertEquals(3L, searchResponse.getHits().getTotalHits().value()); + } + + @Test + void test_update_by_query() throws Exception { + final String index = "test_update_by_query"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 3; i++) { + bulkRequestBuilder.add(client.prepareIndex().setIndex(index).setId(String.valueOf(i)) + .setSource("{\"value\":" + i + ",\"status\":\"new\"}", XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(index).execute().actionGet(); + + final org.opensearch.index.reindex.UpdateByQueryRequest request = new org.opensearch.index.reindex.UpdateByQueryRequest(index); + request.setQuery(QueryBuilders.matchAllQuery()); + request.setScript(new org.opensearch.script.Script("ctx._source.status = 'updated'")); + request.setConflicts("proceed"); + request.setRefresh(true); + final org.opensearch.index.reindex.BulkByScrollResponse response = + client.execute(org.opensearch.index.reindex.UpdateByQueryAction.INSTANCE, request).actionGet(); + assertEquals(3L, response.getTotal()); + assertEquals(3L, response.getUpdated()); + assertEquals(0, response.getBulkFailures().size()); + + final SearchResponse searchResponse = + client.prepareSearch(index).setQuery(QueryBuilders.matchQuery("status", "updated")).execute().actionGet(); + assertEquals(3L, searchResponse.getHits().getTotalHits().value()); + } + + @Test + void test_delete_by_query() throws Exception { + final String index = "test_delete_by_query"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 5; i++) { + final String group = i <= 2 ? "a" : "b"; + bulkRequestBuilder.add(client.prepareIndex().setIndex(index).setId(String.valueOf(i)) + .setSource("{\"value\":" + i + ",\"group\":\"" + group + "\"}", XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(index).execute().actionGet(); + + final org.opensearch.index.reindex.DeleteByQueryRequest request = new org.opensearch.index.reindex.DeleteByQueryRequest(index); + request.setQuery(QueryBuilders.matchQuery("group", "a")); + request.setConflicts("proceed"); + request.setRefresh(true); + final org.opensearch.index.reindex.BulkByScrollResponse response = + client.execute(org.opensearch.index.reindex.DeleteByQueryAction.INSTANCE, request).actionGet(); + assertEquals(2L, response.getDeleted()); + assertEquals(0, response.getBulkFailures().size()); + + final SearchResponse searchResponse = client.prepareSearch(index).setQuery(QueryBuilders.matchAllQuery()).execute().actionGet(); + assertEquals(3L, searchResponse.getHits().getTotalHits().value()); + } + + @Test + void test_resolve_index() throws Exception { + final String index = "test_resolve_index"; + final String alias = "test_resolve_index_alias"; + client.admin().indices().prepareCreate(index).addAlias(new Alias(alias)).execute().actionGet(); + + final org.opensearch.action.admin.indices.resolve.ResolveIndexAction.Request request = + new org.opensearch.action.admin.indices.resolve.ResolveIndexAction.Request(new String[] { "test_resolve_index*" }); + final org.opensearch.action.admin.indices.resolve.ResolveIndexAction.Response response = + client.execute(org.opensearch.action.admin.indices.resolve.ResolveIndexAction.INSTANCE, request).actionGet(); + assertTrue(response.getIndices().stream().anyMatch(i -> index.equals(i.getName()))); + assertTrue(response.getAliases().stream().anyMatch(a -> alias.equals(a.getName()))); + } + + @Test + void test_pit_lifecycle() throws Exception { + final String index = "test_pit_lifecycle"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 3; i++) { + bulkRequestBuilder.add( + client.prepareIndex().setIndex(index).setId(String.valueOf(i)).setSource("{\"value\":" + i + "}", XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(index).execute().actionGet(); + + final org.opensearch.action.search.CreatePitRequest createPitRequest = + new org.opensearch.action.search.CreatePitRequest(TimeValue.timeValueMinutes(1), true, index); + final org.opensearch.action.search.CreatePitResponse createPitResponse = + client.execute(org.opensearch.action.search.CreatePitAction.INSTANCE, createPitRequest).actionGet(); + final String pitId = createPitResponse.getId(); + assertNotNull(pitId); + assertFalse(pitId.isEmpty()); + + final org.opensearch.action.search.GetAllPitNodesResponse getAllPitsResponse = client + .execute(org.opensearch.action.search.GetAllPitsAction.INSTANCE, new org.opensearch.action.search.GetAllPitNodesRequest()) + .actionGet(); + assertTrue(getAllPitsResponse.getPitInfos().stream().anyMatch(p -> pitId.equals(p.getPitId()))); + + final org.opensearch.action.search.DeletePitResponse deletePitResponse = client + .execute(org.opensearch.action.search.DeletePitAction.INSTANCE, new org.opensearch.action.search.DeletePitRequest(pitId)) + .actionGet(); + assertTrue(deletePitResponse.getDeletePitResults().stream().allMatch(org.opensearch.action.search.DeletePitInfo::isSuccessful)); + } + + @Test + void test_pit_delete_all() throws Exception { + final String index = "test_pit_delete_all"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 2; i++) { + bulkRequestBuilder.add( + client.prepareIndex().setIndex(index).setId(String.valueOf(i)).setSource("{\"value\":" + i + "}", XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(index).execute().actionGet(); + + final org.opensearch.action.search.CreatePitRequest createPitRequest = + new org.opensearch.action.search.CreatePitRequest(TimeValue.timeValueMinutes(1), true, index); + final org.opensearch.action.search.CreatePitResponse createPitResponse = + client.execute(org.opensearch.action.search.CreatePitAction.INSTANCE, createPitRequest).actionGet(); + assertNotNull(createPitResponse.getId()); + + final org.opensearch.action.search.DeletePitResponse deletePitResponse = client + .execute(org.opensearch.action.search.DeletePitAction.INSTANCE, new org.opensearch.action.search.DeletePitRequest("_all")) + .actionGet(); + assertTrue(deletePitResponse.getDeletePitResults().stream().allMatch(org.opensearch.action.search.DeletePitInfo::isSuccessful)); + + final org.opensearch.action.search.GetAllPitNodesResponse getAllPitsResponse = client + .execute(org.opensearch.action.search.GetAllPitsAction.INSTANCE, new org.opensearch.action.search.GetAllPitNodesRequest()) + .actionGet(); + assertTrue(getAllPitsResponse.getPitInfos().isEmpty()); + } } diff --git a/src/test/java/org/codelibs/fesen/client/OpenSearch3ClientTest.java b/src/test/java/org/codelibs/fesen/client/OpenSearch3ClientTest.java index 82159f8..7b74516 100644 --- a/src/test/java/org/codelibs/fesen/client/OpenSearch3ClientTest.java +++ b/src/test/java/org/codelibs/fesen/client/OpenSearch3ClientTest.java @@ -2309,4 +2309,155 @@ void test_wlmStats_withNodeFilter() throws Exception { })); latch.await(); } + + @Test + void test_reindex() throws Exception { + final String srcIndex = "test_reindex_src"; + final String destIndex = "test_reindex_dest"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 3; i++) { + bulkRequestBuilder.add(client.prepareIndex().setIndex(srcIndex).setId(String.valueOf(i)).setSource("{\"value\":" + i + "}", + XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(srcIndex).execute().actionGet(); + + final org.opensearch.index.reindex.ReindexRequest reindexRequest = new org.opensearch.index.reindex.ReindexRequest(); + reindexRequest.setSourceIndices(srcIndex); + reindexRequest.setDestIndex(destIndex); + reindexRequest.setRefresh(true); + final org.opensearch.index.reindex.BulkByScrollResponse response = + client.execute(org.opensearch.index.reindex.ReindexAction.INSTANCE, reindexRequest).actionGet(); + assertEquals(3L, response.getTotal()); + assertEquals(3L, response.getCreated()); + assertEquals(0, response.getBulkFailures().size()); + + client.admin().indices().prepareRefresh(destIndex).execute().actionGet(); + final SearchResponse searchResponse = client.prepareSearch(destIndex).setQuery(QueryBuilders.matchAllQuery()).execute().actionGet(); + assertEquals(3L, searchResponse.getHits().getTotalHits().value()); + } + + @Test + void test_update_by_query() throws Exception { + final String index = "test_update_by_query"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 3; i++) { + bulkRequestBuilder.add(client.prepareIndex().setIndex(index).setId(String.valueOf(i)) + .setSource("{\"value\":" + i + ",\"status\":\"new\"}", XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(index).execute().actionGet(); + + final org.opensearch.index.reindex.UpdateByQueryRequest request = new org.opensearch.index.reindex.UpdateByQueryRequest(index); + request.setQuery(QueryBuilders.matchAllQuery()); + request.setScript(new org.opensearch.script.Script("ctx._source.status = 'updated'")); + request.setConflicts("proceed"); + request.setRefresh(true); + final org.opensearch.index.reindex.BulkByScrollResponse response = + client.execute(org.opensearch.index.reindex.UpdateByQueryAction.INSTANCE, request).actionGet(); + assertEquals(3L, response.getTotal()); + assertEquals(3L, response.getUpdated()); + assertEquals(0, response.getBulkFailures().size()); + + final SearchResponse searchResponse = + client.prepareSearch(index).setQuery(QueryBuilders.matchQuery("status", "updated")).execute().actionGet(); + assertEquals(3L, searchResponse.getHits().getTotalHits().value()); + } + + @Test + void test_delete_by_query() throws Exception { + final String index = "test_delete_by_query"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 5; i++) { + final String group = i <= 2 ? "a" : "b"; + bulkRequestBuilder.add(client.prepareIndex().setIndex(index).setId(String.valueOf(i)) + .setSource("{\"value\":" + i + ",\"group\":\"" + group + "\"}", XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(index).execute().actionGet(); + + final org.opensearch.index.reindex.DeleteByQueryRequest request = new org.opensearch.index.reindex.DeleteByQueryRequest(index); + request.setQuery(QueryBuilders.matchQuery("group", "a")); + request.setConflicts("proceed"); + request.setRefresh(true); + final org.opensearch.index.reindex.BulkByScrollResponse response = + client.execute(org.opensearch.index.reindex.DeleteByQueryAction.INSTANCE, request).actionGet(); + assertEquals(2L, response.getDeleted()); + assertEquals(0, response.getBulkFailures().size()); + + final SearchResponse searchResponse = client.prepareSearch(index).setQuery(QueryBuilders.matchAllQuery()).execute().actionGet(); + assertEquals(3L, searchResponse.getHits().getTotalHits().value()); + } + + @Test + void test_resolve_index() throws Exception { + final String index = "test_resolve_index"; + final String alias = "test_resolve_index_alias"; + client.admin().indices().prepareCreate(index).addAlias(new Alias(alias)).execute().actionGet(); + + final org.opensearch.action.admin.indices.resolve.ResolveIndexAction.Request request = + new org.opensearch.action.admin.indices.resolve.ResolveIndexAction.Request(new String[] { "test_resolve_index*" }); + final org.opensearch.action.admin.indices.resolve.ResolveIndexAction.Response response = + client.execute(org.opensearch.action.admin.indices.resolve.ResolveIndexAction.INSTANCE, request).actionGet(); + assertTrue(response.getIndices().stream().anyMatch(i -> index.equals(i.getName()))); + assertTrue(response.getAliases().stream().anyMatch(a -> alias.equals(a.getName()))); + } + + @Test + void test_pit_lifecycle() throws Exception { + final String index = "test_pit_lifecycle"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 3; i++) { + bulkRequestBuilder.add( + client.prepareIndex().setIndex(index).setId(String.valueOf(i)).setSource("{\"value\":" + i + "}", XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(index).execute().actionGet(); + + final org.opensearch.action.search.CreatePitRequest createPitRequest = + new org.opensearch.action.search.CreatePitRequest(TimeValue.timeValueMinutes(1), true, index); + final org.opensearch.action.search.CreatePitResponse createPitResponse = + client.execute(org.opensearch.action.search.CreatePitAction.INSTANCE, createPitRequest).actionGet(); + final String pitId = createPitResponse.getId(); + assertNotNull(pitId); + assertFalse(pitId.isEmpty()); + + final org.opensearch.action.search.GetAllPitNodesResponse getAllPitsResponse = client + .execute(org.opensearch.action.search.GetAllPitsAction.INSTANCE, new org.opensearch.action.search.GetAllPitNodesRequest()) + .actionGet(); + assertTrue(getAllPitsResponse.getPitInfos().stream().anyMatch(p -> pitId.equals(p.getPitId()))); + + final org.opensearch.action.search.DeletePitResponse deletePitResponse = client + .execute(org.opensearch.action.search.DeletePitAction.INSTANCE, new org.opensearch.action.search.DeletePitRequest(pitId)) + .actionGet(); + assertTrue(deletePitResponse.getDeletePitResults().stream().allMatch(org.opensearch.action.search.DeletePitInfo::isSuccessful)); + } + + @Test + void test_pit_delete_all() throws Exception { + final String index = "test_pit_delete_all"; + final BulkRequestBuilder bulkRequestBuilder = client.prepareBulk(); + for (int i = 1; i <= 2; i++) { + bulkRequestBuilder.add( + client.prepareIndex().setIndex(index).setId(String.valueOf(i)).setSource("{\"value\":" + i + "}", XContentType.JSON)); + } + bulkRequestBuilder.execute().actionGet(); + client.admin().indices().prepareRefresh(index).execute().actionGet(); + + final org.opensearch.action.search.CreatePitRequest createPitRequest = + new org.opensearch.action.search.CreatePitRequest(TimeValue.timeValueMinutes(1), true, index); + final org.opensearch.action.search.CreatePitResponse createPitResponse = + client.execute(org.opensearch.action.search.CreatePitAction.INSTANCE, createPitRequest).actionGet(); + assertNotNull(createPitResponse.getId()); + + final org.opensearch.action.search.DeletePitResponse deletePitResponse = client + .execute(org.opensearch.action.search.DeletePitAction.INSTANCE, new org.opensearch.action.search.DeletePitRequest("_all")) + .actionGet(); + assertTrue(deletePitResponse.getDeletePitResults().stream().allMatch(org.opensearch.action.search.DeletePitInfo::isSuccessful)); + + final org.opensearch.action.search.GetAllPitNodesResponse getAllPitsResponse = client + .execute(org.opensearch.action.search.GetAllPitsAction.INSTANCE, new org.opensearch.action.search.GetAllPitNodesRequest()) + .actionGet(); + assertTrue(getAllPitsResponse.getPitInfos().isEmpty()); + } } diff --git a/src/test/java/org/codelibs/fesen/client/action/ActionTestUtils.java b/src/test/java/org/codelibs/fesen/client/action/ActionTestUtils.java index 3fc72bc..a619992 100644 --- a/src/test/java/org/codelibs/fesen/client/action/ActionTestUtils.java +++ b/src/test/java/org/codelibs/fesen/client/action/ActionTestUtils.java @@ -158,6 +158,23 @@ static Map params(final CurlRequest request) { return map; } + /** + * Returns the request URL of the given curl request, read reflectively from the + * protected {@code url} field. + * + * @param request the curl request to inspect + * @return the request URL + */ + static String url(final CurlRequest request) { + try { + final Field field = CurlRequest.class.getDeclaredField("url"); + field.setAccessible(true); + return (String) field.get(request); + } catch (final ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + private static String decode(final String value) { return URLDecoder.decode(value, StandardCharsets.UTF_8); } diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpCreatePitActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpCreatePitActionTest.java index d7d123e..9df51d3 100644 --- a/src/test/java/org/codelibs/fesen/client/action/HttpCreatePitActionTest.java +++ b/src/test/java/org/codelibs/fesen/client/action/HttpCreatePitActionTest.java @@ -16,14 +16,24 @@ package org.codelibs.fesen.client.action; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import org.codelibs.fesen.client.EngineInfo.EngineType; import org.junit.jupiter.api.Test; import org.opensearch.action.search.CreatePitAction; +import org.opensearch.action.search.CreatePitRequest; import org.opensearch.action.search.CreatePitResponse; +import org.opensearch.common.unit.TimeValue; import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.action.ActionListener; import org.opensearch.core.xcontent.DeprecationHandler; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.XContentParser; @@ -32,6 +42,16 @@ class HttpCreatePitActionTest { private final HttpCreatePitAction action = new HttpCreatePitAction(null, CreatePitAction.INSTANCE); + private static HttpCreatePitAction action(final EngineType engineType) { + return new HttpCreatePitAction(ActionTestUtils.testClient(engineType), CreatePitAction.INSTANCE); + } + + private static CreatePitRequest request() { + return new CreatePitRequest(TimeValue.timeValueMinutes(5), true, "idx"); + } + + // --- OpenSearch fromXContent (unchanged behavior) --- + @Test void test_fromXContent() throws IOException { final String json = "{\"pit_id\":\"abc\",\"_shards\":{\"total\":2,\"successful\":2,\"skipped\":0,\"failed\":0}," @@ -48,4 +68,90 @@ void test_fromXContent() throws IOException { assertEquals(0, response.getFailedShards()); } } + + // --- Per-engine getCurlRequest --- + + @Test + void test_getCurlRequest_openSearch() { + for (final EngineType engineType : new EngineType[] { EngineType.OPENSEARCH2, EngineType.OPENSEARCH3 }) { + final HttpCreatePitAction osAction = action(engineType); + final CreatePitRequest request = request(); + final var curlRequest = osAction.getCurlRequest(request); + assertTrue(ActionTestUtils.url(curlRequest).endsWith("/idx/_search/point_in_time"), engineType.toString()); + final Map params = ActionTestUtils.params(curlRequest); + assertEquals("true", params.get("allow_partial_pit_creation"), engineType.toString()); + assertEquals("5m", params.get("keep_alive"), engineType.toString()); + assertFalse(params.containsKey("allow_partial_search_results"), engineType.toString()); + } + } + + @Test + void test_getCurlRequest_elasticsearch() { + for (final EngineType engineType : new EngineType[] { EngineType.ELASTICSEARCH7, EngineType.ELASTICSEARCH8 }) { + final HttpCreatePitAction esAction = action(engineType); + final CreatePitRequest request = request(); + final var curlRequest = esAction.getCurlRequest(request); + assertTrue(ActionTestUtils.url(curlRequest).endsWith("/idx/_pit"), engineType.toString()); + final Map params = ActionTestUtils.params(curlRequest); + assertEquals("5m", params.get("keep_alive"), engineType.toString()); + // The Elasticsearch _pit endpoint rejects both partial-results parameters (HTTP 400), + // so neither is sent. + assertFalse(params.containsKey("allow_partial_search_results"), engineType.toString()); + assertFalse(params.containsKey("allow_partial_pit_creation"), engineType.toString()); + } + } + + // --- Gating --- + + @Test + void test_execute_unsupportedEngine() { + // OpenSearch 1.x predates PIT; testClient cannot stub UNKNOWN (its stub falls back to OpenSearch 3.x). + final HttpCreatePitAction gatedAction = action(EngineType.OPENSEARCH1); + final AtomicReference failure = new AtomicReference<>(); + gatedAction.execute(request(), ActionListener.wrap(r -> {}, failure::set)); + assertInstanceOf(UnsupportedOperationException.class, failure.get()); + } + + // --- Elasticsearch response parsing --- + + @Test + void test_parseEsCreate_idOnly() throws IOException { + final String json = "{\"id\":\"abc\"}"; + try (final XContentParser parser = + JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) { + final CreatePitResponse response = action.parseEsCreate(parser); + assertNotNull(response); + assertEquals("abc", response.getId()); + assertEquals(0, response.getTotalShards()); + assertEquals(0, response.getSuccessfulShards()); + assertEquals(0, response.getSkippedShards()); + assertEquals(0, response.getFailedShards()); + } + } + + @Test + void test_parseEsCreate_withShards() throws IOException { + final String json = "{\"id\":\"abc\",\"_shards\":{\"total\":5,\"successful\":5,\"skipped\":0,\"failed\":0}}"; + try (final XContentParser parser = + JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) { + final CreatePitResponse response = action.parseEsCreate(parser); + assertNotNull(response); + assertEquals("abc", response.getId()); + assertEquals(5, response.getTotalShards()); + assertEquals(5, response.getSuccessfulShards()); + assertEquals(0, response.getSkippedShards()); + assertEquals(0, response.getFailedShards()); + } + } + + @Test + void test_parseEsCreate_errorBody_throws() throws IOException { + // An error body (routed through the success path) must surface as a failure, not become a + // response with a null id. + final String json = "{\"error\":{\"type\":\"illegal_argument_exception\"},\"status\":400}"; + try (final XContentParser parser = + JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) { + assertThrows(IOException.class, () -> action.parseEsCreate(parser)); + } + } } diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpDeleteByQueryActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpDeleteByQueryActionTest.java index 0e82753..60420b9 100644 --- a/src/test/java/org/codelibs/fesen/client/action/HttpDeleteByQueryActionTest.java +++ b/src/test/java/org/codelibs/fesen/client/action/HttpDeleteByQueryActionTest.java @@ -18,6 +18,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; import org.junit.jupiter.api.Test; import org.opensearch.common.xcontent.json.JsonXContent; @@ -26,11 +29,47 @@ import org.opensearch.core.xcontent.XContentParser; import org.opensearch.index.reindex.BulkByScrollResponse; import org.opensearch.index.reindex.DeleteByQueryAction; +import org.opensearch.index.reindex.DeleteByQueryRequest; class HttpDeleteByQueryActionTest { private final HttpDeleteByQueryAction action = new HttpDeleteByQueryAction(null, DeleteByQueryAction.INSTANCE); + private final HttpDeleteByQueryAction clientAction = + new HttpDeleteByQueryAction(ActionTestUtils.testClient(), DeleteByQueryAction.INSTANCE); + + @Test + void test_getCurlRequest_endpointAndParams() { + final DeleteByQueryRequest request = new DeleteByQueryRequest("idx"); + request.setAbortOnVersionConflict(false); + request.setMaxDocs(30); + request.setSlices(2); + request.setRequestsPerSecond(100f); + request.setRouting("r2"); + final Map params = ActionTestUtils.params(clientAction.getCurlRequest(request)); + // Endpoint is POST /idx/_delete_by_query. + assertTrue(ActionTestUtils.url(clientAction.getCurlRequest(request)).contains("/idx/_delete_by_query")); + assertEquals("true", params.get("wait_for_completion")); + assertEquals("proceed", params.get("conflicts")); + assertEquals("30", params.get("max_docs")); + assertEquals("2", params.get("slices")); + assertEquals(Float.toString(request.getRequestsPerSecond()), params.get("requests_per_second")); + assertEquals("r2", params.get("routing")); + // delete-by-query has no pipeline parameter (unlike update-by-query). + assertFalse(params.containsKey("pipeline")); + } + + @Test + void test_getCurlRequest_defaultsOmitOptionalParams() { + final DeleteByQueryRequest request = new DeleteByQueryRequest("idx"); + final Map params = ActionTestUtils.params(clientAction.getCurlRequest(request)); + assertEquals("true", params.get("wait_for_completion")); + assertFalse(params.containsKey("conflicts")); + assertFalse(params.containsKey("max_docs")); + assertFalse(params.containsKey("slices")); + assertFalse(params.containsKey("routing")); + } + @Test void test_fromXContent() throws Exception { final String json = "{\"took\":210,\"timed_out\":false,\"total\":75,\"updated\":0,\"created\":0,\"deleted\":75," diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpDeletePitActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpDeletePitActionTest.java index 4d1e373..b0e5c91 100644 --- a/src/test/java/org/codelibs/fesen/client/action/HttpDeletePitActionTest.java +++ b/src/test/java/org/codelibs/fesen/client/action/HttpDeletePitActionTest.java @@ -16,15 +16,22 @@ package org.codelibs.fesen.client.action; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import java.util.concurrent.atomic.AtomicReference; +import org.codelibs.fesen.client.EngineInfo.EngineType; import org.junit.jupiter.api.Test; import org.opensearch.action.search.DeletePitAction; +import org.opensearch.action.search.DeletePitRequest; import org.opensearch.action.search.DeletePitResponse; import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.action.ActionListener; import org.opensearch.core.xcontent.DeprecationHandler; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.XContentParser; @@ -33,6 +40,12 @@ class HttpDeletePitActionTest { private final HttpDeletePitAction action = new HttpDeletePitAction(null, DeletePitAction.INSTANCE); + private static HttpDeletePitAction action(final EngineType engineType) { + return new HttpDeletePitAction(ActionTestUtils.testClient(engineType), DeletePitAction.INSTANCE); + } + + // --- OpenSearch fromXContent (unchanged behavior) --- + @Test void test_fromXContent() throws IOException { final String json = "{\"pits\":[{\"successful\":true,\"pit_id\":\"abc\"},{\"successful\":false,\"pit_id\":\"def\"}]}"; @@ -47,4 +60,104 @@ void test_fromXContent() throws IOException { assertEquals("def", response.getDeletePitResults().get(1).getPitId()); } } + + // --- Per-engine request bodies --- + + @Test + void test_buildBody_openSearch() { + final DeletePitRequest request = new DeletePitRequest("a", "b"); + final String body = action(EngineType.OPENSEARCH3).buildBody(request); + assertTrue(body.contains("\"pit_id\""), body); + assertTrue(body.contains("\"a\""), body); + assertTrue(body.contains("\"b\""), body); + } + + @Test + void test_buildEsBody_elasticsearch() { + // The Elasticsearch _pit endpoint accepts a single id (a JSON string, not an array). + final DeletePitRequest request = new DeletePitRequest("a"); + final String body = action(EngineType.ELASTICSEARCH8).buildEsBody(request); + assertTrue(body.contains("\"id\""), body); + assertFalse(body.contains("\"pit_id\""), body); + assertTrue(body.contains("\"a\""), body); + assertFalse(body.contains("["), body); + } + + // --- Per-engine getCurlRequest endpoints --- + + @Test + void test_getCurlRequest_openSearch() { + final DeletePitRequest request = new DeletePitRequest("a", "b"); + final String url = ActionTestUtils.url(action(EngineType.OPENSEARCH3).getCurlRequest(request)); + assertTrue(url.endsWith("/_search/point_in_time"), url); + } + + @Test + void test_getCurlRequest_elasticsearch() { + final DeletePitRequest request = new DeletePitRequest("a", "b"); + final String url = ActionTestUtils.url(action(EngineType.ELASTICSEARCH8).getCurlRequest(request)); + assertTrue(url.endsWith("/_pit"), url); + } + + @Test + void test_getCurlRequest_deleteAll_openSearch() { + final DeletePitRequest request = new DeletePitRequest("_all"); + final String url = ActionTestUtils.url(action(EngineType.OPENSEARCH3).getCurlRequest(request)); + assertTrue(url.endsWith("/_search/point_in_time/_all"), url); + } + + // --- Gating --- + + @Test + void test_execute_unsupportedEngine() { + // OpenSearch 1.x predates PIT; testClient cannot stub UNKNOWN (its stub falls back to OpenSearch 3.x). + final AtomicReference failure = new AtomicReference<>(); + action(EngineType.OPENSEARCH1).execute(new DeletePitRequest("a"), ActionListener.wrap(r -> {}, failure::set)); + assertInstanceOf(UnsupportedOperationException.class, failure.get()); + } + + @Test + void test_execute_deleteAll_elasticsearchUnsupported() { + final AtomicReference failure = new AtomicReference<>(); + action(EngineType.ELASTICSEARCH8).execute(new DeletePitRequest("_all"), ActionListener.wrap(r -> {}, failure::set)); + assertInstanceOf(UnsupportedOperationException.class, failure.get()); + } + + @Test + void test_execute_multipleIds_elasticsearchUnsupported() { + // Elasticsearch _pit deletes a single id per request; multiple ids are rejected up front. + final AtomicReference failure = new AtomicReference<>(); + action(EngineType.ELASTICSEARCH8).execute(new DeletePitRequest("a", "b"), ActionListener.wrap(r -> {}, failure::set)); + assertInstanceOf(UnsupportedOperationException.class, failure.get()); + } + + // --- Elasticsearch response parsing --- + + @Test + void test_parseEsDelete() throws IOException { + final String json = "{\"succeeded\":true,\"num_freed\":2}"; + final DeletePitRequest request = new DeletePitRequest("a", "b"); + try (final XContentParser parser = + JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) { + final DeletePitResponse response = action.parseEsDelete(parser, request); + assertNotNull(response); + assertEquals(2, response.getDeletePitResults().size()); + assertTrue(response.getDeletePitResults().get(0).isSuccessful()); + assertTrue(response.getDeletePitResults().get(1).isSuccessful()); + assertEquals("a", response.getDeletePitResults().get(0).getPitId()); + assertEquals("b", response.getDeletePitResults().get(1).getPitId()); + } + } + + @Test + void test_parseEsDelete_errorBody_throws() throws IOException { + // An error body (routed through the success path) must surface as a failure, not be reported + // as an unsuccessful deletion of the requested ids. + final String json = "{\"error\":{\"type\":\"illegal_argument_exception\"},\"status\":400}"; + final DeletePitRequest request = new DeletePitRequest("a"); + try (final XContentParser parser = + JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) { + assertThrows(IOException.class, () -> action.parseEsDelete(parser, request)); + } + } } diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpGetAllPitsActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpGetAllPitsActionTest.java index 98c4fa4..6bcdfb8 100644 --- a/src/test/java/org/codelibs/fesen/client/action/HttpGetAllPitsActionTest.java +++ b/src/test/java/org/codelibs/fesen/client/action/HttpGetAllPitsActionTest.java @@ -16,14 +16,20 @@ package org.codelibs.fesen.client.action; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import java.util.concurrent.atomic.AtomicReference; +import org.codelibs.fesen.client.EngineInfo.EngineType; import org.junit.jupiter.api.Test; +import org.opensearch.action.search.GetAllPitNodesRequest; import org.opensearch.action.search.GetAllPitNodesResponse; import org.opensearch.action.search.GetAllPitsAction; import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.action.ActionListener; import org.opensearch.core.xcontent.DeprecationHandler; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.core.xcontent.XContentParser; @@ -32,6 +38,12 @@ class HttpGetAllPitsActionTest { private final HttpGetAllPitsAction action = new HttpGetAllPitsAction(null, GetAllPitsAction.INSTANCE); + private static HttpGetAllPitsAction action(final EngineType engineType) { + return new HttpGetAllPitsAction(ActionTestUtils.testClient(engineType), GetAllPitsAction.INSTANCE); + } + + // --- OpenSearch fromXContent (unchanged behavior) --- + @Test void test_fromXContent() throws IOException { final String json = "{\"pits\":[{\"pit_id\":\"abc\",\"creation_time\":123,\"keep_alive\":300000}]}"; @@ -45,4 +57,27 @@ void test_fromXContent() throws IOException { assertEquals(300000L, response.getPitInfos().get(0).getKeepAlive()); } } + + // --- Per-engine getCurlRequest --- + + @Test + void test_getCurlRequest_openSearch() { + for (final EngineType engineType : new EngineType[] { EngineType.OPENSEARCH2, EngineType.OPENSEARCH3 }) { + final String url = ActionTestUtils.url(action(engineType).getCurlRequest(new GetAllPitNodesRequest())); + assertTrue(url.endsWith("/_search/point_in_time/_all"), engineType + ": " + url); + } + } + + // --- Gating --- + + @Test + void test_execute_unsupportedEngine() { + // Listing all PITs is OpenSearch 2.x+ only; testClient cannot stub UNKNOWN (its stub falls back to OpenSearch 3.x). + for (final EngineType engineType : new EngineType[] { EngineType.OPENSEARCH1, EngineType.ELASTICSEARCH7, + EngineType.ELASTICSEARCH8 }) { + final AtomicReference failure = new AtomicReference<>(); + action(engineType).execute(new GetAllPitNodesRequest(), ActionListener.wrap(r -> {}, failure::set)); + assertInstanceOf(UnsupportedOperationException.class, failure.get(), engineType.toString()); + } + } } diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpGetMappingsActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpGetMappingsActionTest.java new file mode 100644 index 0000000..f7f5f4c --- /dev/null +++ b/src/test/java/org/codelibs/fesen/client/action/HttpGetMappingsActionTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +package org.codelibs.fesen.client.action; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.Map; + +import org.codelibs.fesen.client.EngineInfo.EngineType; +import org.junit.jupiter.api.Test; +import org.opensearch.action.admin.indices.mapping.get.GetMappingsAction; +import org.opensearch.action.admin.indices.mapping.get.GetMappingsRequest; +import org.opensearch.action.admin.indices.mapping.get.GetMappingsResponse; +import org.opensearch.action.support.IndicesOptions; +import org.opensearch.cluster.metadata.MappingMetadata; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.xcontent.DeprecationHandler; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentParser; + +class HttpGetMappingsActionTest { + + private final HttpGetMappingsAction os3Action = + new HttpGetMappingsAction(ActionTestUtils.testClient(EngineType.OPENSEARCH3), GetMappingsAction.INSTANCE); + + private final HttpGetMappingsAction es8Action = + new HttpGetMappingsAction(ActionTestUtils.testClient(EngineType.ELASTICSEARCH8), GetMappingsAction.INSTANCE); + + @Test + void test_getCurlRequest_localPresentOnOpenSearch3() { + final GetMappingsRequest request = new GetMappingsRequest().indices("test-index"); + final Map params = ActionTestUtils.params(os3Action.getCurlRequest(request)); + // The index is carried in the path, and the mapping endpoint is used. + assertTrue(ActionTestUtils.url(os3Action.getCurlRequest(request)).contains("/test-index/_mapping")); + // local is emitted for every engine except Elasticsearch 8.x. + assertTrue(params.containsKey("local")); + assertEquals(Boolean.toString(request.local()), params.get("local")); + } + + @Test + void test_getCurlRequest_localAbsentOnElasticsearch8() { + final GetMappingsRequest request = new GetMappingsRequest().indices("test-index"); + final Map params = ActionTestUtils.params(es8Action.getCurlRequest(request)); + assertTrue(ActionTestUtils.url(es8Action.getCurlRequest(request)).contains("/test-index/_mapping")); + // Elasticsearch 8.x rejects the local parameter on the mapping endpoint, so the action suppresses it. + assertFalse(params.containsKey("local")); + // Indices options are still forwarded on Elasticsearch 8.x. + assertTrue(params.containsKey("ignore_unavailable")); + assertTrue(params.containsKey("allow_no_indices")); + assertTrue(params.containsKey("expand_wildcards")); + } + + @Test + void test_getCurlRequest_indicesOptionsForwarded() { + final GetMappingsRequest request = new GetMappingsRequest().indices("test-index"); + request.indicesOptions(IndicesOptions.fromOptions(true, false, false, true)); + final Map params = ActionTestUtils.params(os3Action.getCurlRequest(request)); + assertEquals("true", params.get("ignore_unavailable")); + assertEquals("false", params.get("allow_no_indices")); + assertEquals("closed", params.get("expand_wildcards")); + } + + @Test + void test_fromXContent_skipsDynamicTemplates() throws IOException { + // OpenSearch mapping response: properties plus a dynamic_templates block that must be skipped. + final String json = "{\"test-index\":{\"mappings\":{" // + + "\"properties\":{\"field1\":{\"type\":\"text\"}}," // + + "\"dynamic_templates\":[{\"strings\":{\"match_mapping_type\":\"string\"," // + + "\"mapping\":{\"type\":\"keyword\"}}}]}}}"; + try (final XContentParser parser = + JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) { + final GetMappingsResponse response = HttpGetMappingsAction.fromXContent(parser); + assertNotNull(response); + final Map mappings = response.mappings(); + // dynamic_templates is skipped for compatibility with older versions. + assertFalse(mappings.containsKey("dynamic_templates")); + assertEquals(1, mappings.size()); + } + } +} diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpReindexActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpReindexActionTest.java index 9b7aad4..781f4c3 100644 --- a/src/test/java/org/codelibs/fesen/client/action/HttpReindexActionTest.java +++ b/src/test/java/org/codelibs/fesen/client/action/HttpReindexActionTest.java @@ -18,6 +18,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; import org.junit.jupiter.api.Test; import org.opensearch.common.xcontent.json.JsonXContent; @@ -26,11 +29,72 @@ import org.opensearch.core.xcontent.XContentParser; import org.opensearch.index.reindex.BulkByScrollResponse; import org.opensearch.index.reindex.ReindexAction; +import org.opensearch.index.reindex.ReindexRequest; class HttpReindexActionTest { private final HttpReindexAction action = new HttpReindexAction(null, ReindexAction.INSTANCE); + private final HttpReindexAction clientAction = new HttpReindexAction(ActionTestUtils.testClient(), ReindexAction.INSTANCE); + + private static ReindexRequest reindexRequest() { + final ReindexRequest request = new ReindexRequest(); + request.setSourceIndices("src"); + request.setDestIndex("dst"); + return request; + } + + @Test + void test_getCurlRequest_endpointAndCommonParams() { + final ReindexRequest request = reindexRequest(); + request.setRefresh(true); + request.setSlices(3); + request.setRequestsPerSecond(500f); + request.setWaitForActiveShards(2); + request.setMaxDocs(25); + request.setAbortOnVersionConflict(false); + final Map params = ActionTestUtils.params(clientAction.getCurlRequest(request)); + // Endpoint is POST /_reindex with no index in the path. + assertTrue(ActionTestUtils.url(clientAction.getCurlRequest(request)).endsWith("/_reindex")); + // Critical: wait_for_completion is always forced so the body can be parsed synchronously. + assertEquals("true", params.get("wait_for_completion")); + assertEquals("true", params.get("refresh")); + assertEquals("3", params.get("slices")); + assertEquals(Float.toString(request.getRequestsPerSecond()), params.get("requests_per_second")); + assertEquals("2", params.get("wait_for_active_shards")); + // For reindex, max_docs and conflicts ride in the BODY, never as query parameters. + assertFalse(params.containsKey("max_docs")); + assertFalse(params.containsKey("conflicts")); + } + + @Test + void test_getCurlRequest_body_carriesSourceDestMaxDocsConflicts() { + final ReindexRequest request = reindexRequest(); + request.setMaxDocs(25); + request.setAbortOnVersionConflict(false); + final String body = clientAction.toSource(request); + assertTrue(body.contains("\"source\"")); + assertTrue(body.contains("src")); + assertTrue(body.contains("\"dest\"")); + assertTrue(body.contains("dst")); + assertTrue(body.contains("\"max_docs\"")); + assertTrue(body.contains("\"conflicts\"")); + } + + @Test + void test_getCurlRequest_waitForCompletionAlwaysTrueAndNoLeakedParams() { + final ReindexRequest request = reindexRequest(); + final Map params = ActionTestUtils.params(clientAction.getCurlRequest(request)); + assertEquals("true", params.get("wait_for_completion")); + // Optional parameters are omitted when unset. + assertFalse(params.containsKey("refresh")); + assertFalse(params.containsKey("slices")); + assertFalse(params.containsKey("requests_per_second")); + assertFalse(params.containsKey("wait_for_active_shards")); + assertFalse(params.containsKey("conflicts")); + assertFalse(params.containsKey("max_docs")); + } + @Test void test_fromXContent() throws Exception { final String json = "{\"took\":147,\"timed_out\":false,\"total\":120,\"updated\":0,\"created\":120,\"deleted\":0," diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpResolveIndexActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpResolveIndexActionTest.java index 04abed4..db10981 100644 --- a/src/test/java/org/codelibs/fesen/client/action/HttpResolveIndexActionTest.java +++ b/src/test/java/org/codelibs/fesen/client/action/HttpResolveIndexActionTest.java @@ -18,14 +18,17 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; +import java.util.Map; import org.junit.jupiter.api.Test; import org.opensearch.action.admin.indices.resolve.ResolveIndexAction; import org.opensearch.action.admin.indices.resolve.ResolveIndexAction.ResolvedAlias; import org.opensearch.action.admin.indices.resolve.ResolveIndexAction.ResolvedDataStream; import org.opensearch.action.admin.indices.resolve.ResolveIndexAction.ResolvedIndex; +import org.opensearch.action.support.IndicesOptions; import org.opensearch.common.xcontent.json.JsonXContent; import org.opensearch.core.xcontent.DeprecationHandler; import org.opensearch.core.xcontent.NamedXContentRegistry; @@ -35,6 +38,21 @@ class HttpResolveIndexActionTest { private final HttpResolveIndexAction action = new HttpResolveIndexAction(null, ResolveIndexAction.INSTANCE); + private final HttpResolveIndexAction clientAction = + new HttpResolveIndexAction(ActionTestUtils.testClient(), ResolveIndexAction.INSTANCE); + + @Test + void test_getCurlRequest_urlAndIndicesOptions() { + final ResolveIndexAction.Request request = + new ResolveIndexAction.Request(new String[] { "idx1", "idx2" }, IndicesOptions.fromOptions(true, false, false, true)); + // The index pattern is carried in the path under /_resolve/index/. + assertTrue(ActionTestUtils.url(clientAction.getCurlRequest(request)).contains("/_resolve/index/idx1,idx2")); + final Map params = ActionTestUtils.params(clientAction.getCurlRequest(request)); + assertEquals("true", params.get("ignore_unavailable")); + assertEquals("false", params.get("allow_no_indices")); + assertEquals("closed", params.get("expand_wildcards")); + } + @Test void test_fromXContent() throws IOException { final String json = "{" // diff --git a/src/test/java/org/codelibs/fesen/client/action/HttpUpdateByQueryActionTest.java b/src/test/java/org/codelibs/fesen/client/action/HttpUpdateByQueryActionTest.java index 7418f1d..23eda32 100644 --- a/src/test/java/org/codelibs/fesen/client/action/HttpUpdateByQueryActionTest.java +++ b/src/test/java/org/codelibs/fesen/client/action/HttpUpdateByQueryActionTest.java @@ -18,6 +18,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; import org.junit.jupiter.api.Test; import org.opensearch.common.xcontent.json.JsonXContent; @@ -26,11 +29,50 @@ import org.opensearch.core.xcontent.XContentParser; import org.opensearch.index.reindex.BulkByScrollResponse; import org.opensearch.index.reindex.UpdateByQueryAction; +import org.opensearch.index.reindex.UpdateByQueryRequest; class HttpUpdateByQueryActionTest { private final HttpUpdateByQueryAction action = new HttpUpdateByQueryAction(null, UpdateByQueryAction.INSTANCE); + private final HttpUpdateByQueryAction clientAction = + new HttpUpdateByQueryAction(ActionTestUtils.testClient(), UpdateByQueryAction.INSTANCE); + + @Test + void test_getCurlRequest_endpointAndParams() { + final UpdateByQueryRequest request = new UpdateByQueryRequest("idx"); + request.setAbortOnVersionConflict(false); + request.setMaxDocs(50); + request.setSlices(4); + request.setRequestsPerSecond(250f); + request.setRouting("r1"); + request.setPipeline("p1"); + final Map params = ActionTestUtils.params(clientAction.getCurlRequest(request)); + // Endpoint is POST /idx/_update_by_query. + assertTrue(ActionTestUtils.url(clientAction.getCurlRequest(request)).contains("/idx/_update_by_query")); + assertEquals("true", params.get("wait_for_completion")); + // For by-query, conflicts and max_docs are query PARAMETERS (unlike reindex, where they ride in the body). + assertEquals("proceed", params.get("conflicts")); + assertEquals("50", params.get("max_docs")); + assertEquals("4", params.get("slices")); + assertEquals(Float.toString(request.getRequestsPerSecond()), params.get("requests_per_second")); + assertEquals("r1", params.get("routing")); + assertEquals("p1", params.get("pipeline")); + } + + @Test + void test_getCurlRequest_defaultsOmitOptionalParams() { + final UpdateByQueryRequest request = new UpdateByQueryRequest("idx"); + final Map params = ActionTestUtils.params(clientAction.getCurlRequest(request)); + assertEquals("true", params.get("wait_for_completion")); + // conflicts defaults to abort, so it is not emitted; the remaining optionals stay unset. + assertFalse(params.containsKey("conflicts")); + assertFalse(params.containsKey("max_docs")); + assertFalse(params.containsKey("slices")); + assertFalse(params.containsKey("routing")); + assertFalse(params.containsKey("pipeline")); + } + @Test void test_fromXContent() throws Exception { final String json = "{\"took\":320,\"timed_out\":false,\"total\":100,\"updated\":80,\"created\":0,\"deleted\":0,"