Skip to content

Commit 005ad1c

Browse files
marevolclaude
andauthored
Add comprehensive unit tests for HTTP client actions and utilities (#17)
* feat(test): add unit tests for core utilities, node management, and HTTP actions Add comprehensive unit test coverage for previously untested classes: - UrlUtils: URL encoding and joining with special characters, null handling - Node: availability state, URL construction, thread safety - NodeIterator: round-robin iteration, boundary handling, NoSuchElementException - HttpAction: media type detection, active shard count serialization, exception unwrapping - HttpBulkAction: NDJSON request serialization, bulk response parsing with failures - HttpSearchAction: query source serialization for various query types - HttpIndexAction/HttpDeleteAction: response parsing for CRUD operations - HttpGetAction: basic construction verification - HttpCreateIndexRequest: mapping preparation and delegation pattern https://claude.ai/code/session_014opUrsabrAZEr3Ck3m8kAe * fix: correct test expectation for SearchRequest default source SearchRequest initializes with a default SearchSourceBuilder, so getQuerySource returns "{}" instead of null. Updated test to match actual behavior. * style: apply formatter to test files --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent bdd4332 commit 005ad1c

10 files changed

Lines changed: 974 additions & 0 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* Copyright 2012-2025 CodeLibs Project and the Others.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13+
* either express or implied. See the License for the specific language
14+
* governing permissions and limitations under the License.
15+
*/
16+
package org.codelibs.fesen.client.action;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertNotNull;
20+
import static org.junit.jupiter.api.Assertions.assertThrows;
21+
22+
import java.io.IOException;
23+
24+
import org.junit.jupiter.api.Test;
25+
import org.opensearch.OpenSearchException;
26+
import org.opensearch.action.support.ActiveShardCount;
27+
import org.opensearch.common.xcontent.XContentType;
28+
import org.opensearch.core.xcontent.MediaType;
29+
30+
class HttpActionTest {
31+
32+
private final HttpAction action = new HttpAction(null);
33+
34+
@Test
35+
void test_fromMediaTypeOrFormat_json() {
36+
final MediaType result = HttpAction.fromMediaTypeOrFormat("application/json");
37+
assertEquals(XContentType.JSON, result);
38+
}
39+
40+
@Test
41+
void test_fromMediaTypeOrFormat_jsonWithCharset() {
42+
final MediaType result = HttpAction.fromMediaTypeOrFormat("application/json; charset=UTF-8");
43+
assertEquals(XContentType.JSON, result);
44+
}
45+
46+
@Test
47+
void test_fromMediaTypeOrFormat_yaml() {
48+
final MediaType result = HttpAction.fromMediaTypeOrFormat("application/yaml");
49+
assertEquals(XContentType.YAML, result);
50+
}
51+
52+
@Test
53+
void test_fromMediaTypeOrFormat_cbor() {
54+
final MediaType result = HttpAction.fromMediaTypeOrFormat("application/cbor");
55+
assertEquals(XContentType.CBOR, result);
56+
}
57+
58+
@Test
59+
void test_fromMediaTypeOrFormat_smile() {
60+
final MediaType result = HttpAction.fromMediaTypeOrFormat("application/smile");
61+
assertEquals(XContentType.SMILE, result);
62+
}
63+
64+
@Test
65+
void test_fromMediaTypeOrFormat_null_defaultsToJson() {
66+
final MediaType result = HttpAction.fromMediaTypeOrFormat(null);
67+
assertEquals(XContentType.JSON, result);
68+
}
69+
70+
@Test
71+
void test_fromMediaTypeOrFormat_unknown_defaultsToJson() {
72+
final MediaType result = HttpAction.fromMediaTypeOrFormat("text/plain");
73+
assertEquals(XContentType.JSON, result);
74+
}
75+
76+
@Test
77+
void test_fromMediaTypeOrFormat_subtypeOnly() {
78+
final MediaType result = HttpAction.fromMediaTypeOrFormat("json");
79+
assertEquals(XContentType.JSON, result);
80+
}
81+
82+
@Test
83+
void test_getActiveShardsCountValue_all() throws IOException {
84+
final int value = action.getActiveShardsCountValue(ActiveShardCount.ALL);
85+
assertEquals(-1, value);
86+
}
87+
88+
@Test
89+
void test_getActiveShardsCountValue_default() throws IOException {
90+
final int value = action.getActiveShardsCountValue(ActiveShardCount.DEFAULT);
91+
assertEquals(-2, value);
92+
}
93+
94+
@Test
95+
void test_getActiveShardsCountValue_one() throws IOException {
96+
final int value = action.getActiveShardsCountValue(ActiveShardCount.ONE);
97+
assertEquals(1, value);
98+
}
99+
100+
@Test
101+
void test_getActiveShardsCountValue_none() throws IOException {
102+
final int value = action.getActiveShardsCountValue(ActiveShardCount.NONE);
103+
assertEquals(0, value);
104+
}
105+
106+
@Test
107+
void test_getActiveShardsCountValue_customValue() throws IOException {
108+
final int value = action.getActiveShardsCountValue(ActiveShardCount.from(3));
109+
assertEquals(3, value);
110+
}
111+
112+
@Test
113+
void test_unwrapOpenSearchException_withOpenSearchCause() {
114+
final OpenSearchException cause = new OpenSearchException("inner error");
115+
final Exception wrapper = new RuntimeException("outer", cause);
116+
final boolean[] called = { false };
117+
action.unwrapOpenSearchException(new org.opensearch.core.action.ActionListener<>() {
118+
@Override
119+
public void onResponse(final Object o) {
120+
}
121+
122+
@Override
123+
public void onFailure(final Exception e) {
124+
called[0] = true;
125+
assertEquals(cause, e);
126+
}
127+
}, wrapper);
128+
assertEquals(true, called[0]);
129+
}
130+
131+
@Test
132+
void test_unwrapOpenSearchException_withoutOpenSearchCause() {
133+
final RuntimeException original = new RuntimeException("direct error");
134+
final boolean[] called = { false };
135+
action.unwrapOpenSearchException(new org.opensearch.core.action.ActionListener<>() {
136+
@Override
137+
public void onResponse(final Object o) {
138+
}
139+
140+
@Override
141+
public void onFailure(final Exception e) {
142+
called[0] = true;
143+
assertEquals(original, e);
144+
}
145+
}, original);
146+
assertEquals(true, called[0]);
147+
}
148+
149+
@Test
150+
void test_parseFields_areNotNull() {
151+
assertNotNull(HttpAction.SHARD_FIELD);
152+
assertNotNull(HttpAction.INDEX_FIELD);
153+
assertNotNull(HttpAction.QUERY_FIELD);
154+
assertNotNull(HttpAction.REASON_FIELD);
155+
assertNotNull(HttpAction.ALIASES_FIELD);
156+
assertNotNull(HttpAction.MAPPINGS_FIELD);
157+
assertNotNull(HttpAction.TYPE_FIELD);
158+
assertNotNull(HttpAction.DETAILS_FIELD);
159+
assertNotNull(HttpAction._SHARDS_FIELD);
160+
assertNotNull(HttpAction.TASKS_FIELD);
161+
assertNotNull(HttpAction.TOTAL_FIELD);
162+
assertNotNull(HttpAction.SUCCESSFUL_FIELD);
163+
assertNotNull(HttpAction.FAILED_FIELD);
164+
assertNotNull(HttpAction.FAILURES_FIELD);
165+
}
166+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright 2012-2025 CodeLibs Project and the Others.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13+
* either express or implied. See the License for the specific language
14+
* governing permissions and limitations under the License.
15+
*/
16+
package org.codelibs.fesen.client.action;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertFalse;
20+
import static org.junit.jupiter.api.Assertions.assertNotNull;
21+
import static org.junit.jupiter.api.Assertions.assertTrue;
22+
23+
import java.io.IOException;
24+
25+
import org.junit.jupiter.api.Test;
26+
import org.opensearch.action.bulk.BulkAction;
27+
import org.opensearch.action.bulk.BulkResponse;
28+
import org.opensearch.action.delete.DeleteRequest;
29+
import org.opensearch.action.index.IndexRequest;
30+
import org.opensearch.common.xcontent.XContentType;
31+
import org.opensearch.common.xcontent.json.JsonXContent;
32+
import org.opensearch.core.xcontent.DeprecationHandler;
33+
import org.opensearch.core.xcontent.NamedXContentRegistry;
34+
import org.opensearch.core.xcontent.XContentParser;
35+
36+
class HttpBulkActionTest {
37+
38+
private final HttpBulkAction action = new HttpBulkAction(null, BulkAction.INSTANCE);
39+
40+
@Test
41+
void test_getStringfromDocWriteRequest_indexWithId() {
42+
final IndexRequest request = new IndexRequest("test-index").id("doc1").source("{\"field\":\"value\"}", XContentType.JSON);
43+
final String result = action.getStringfromDocWriteRequest(request);
44+
assertTrue(result.contains("\"index\""));
45+
assertTrue(result.contains("\"_index\":\"test-index\""));
46+
assertTrue(result.contains("\"_id\":\"doc1\""));
47+
}
48+
49+
@Test
50+
void test_getStringfromDocWriteRequest_indexWithoutId() {
51+
final IndexRequest request = new IndexRequest("test-index").source("{\"field\":\"value\"}", XContentType.JSON);
52+
request.id(null);
53+
final String result = action.getStringfromDocWriteRequest(request);
54+
assertTrue(result.contains("\"index\"") || result.contains("\"create\""));
55+
assertTrue(result.contains("\"_index\":\"test-index\""));
56+
assertFalse(result.contains("\"_id\""));
57+
}
58+
59+
@Test
60+
void test_getStringfromDocWriteRequest_delete() {
61+
final DeleteRequest request = new DeleteRequest("test-index", "doc1");
62+
final String result = action.getStringfromDocWriteRequest(request);
63+
assertTrue(result.contains("\"delete\""));
64+
assertTrue(result.contains("\"_index\":\"test-index\""));
65+
assertTrue(result.contains("\"_id\":\"doc1\""));
66+
}
67+
68+
@Test
69+
void test_getStringfromDocWriteRequest_withRouting() {
70+
final IndexRequest request =
71+
new IndexRequest("test-index").id("doc1").routing("r1").source("{\"field\":\"value\"}", XContentType.JSON);
72+
final String result = action.getStringfromDocWriteRequest(request);
73+
assertTrue(result.contains("\"routing\":\"r1\""));
74+
}
75+
76+
@Test
77+
void test_getStringfromDocWriteRequest_withPipeline() {
78+
final IndexRequest request =
79+
new IndexRequest("test-index").id("doc1").setPipeline("my-pipeline").source("{\"field\":\"value\"}", XContentType.JSON);
80+
final String result = action.getStringfromDocWriteRequest(request);
81+
assertTrue(result.contains("\"pipeline\":\"my-pipeline\""));
82+
}
83+
84+
@Test
85+
void test_fromXContent_emptyBulkResponse() throws IOException {
86+
final String json = "{\"took\":10,\"errors\":false,\"items\":[]}";
87+
try (final XContentParser parser =
88+
JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) {
89+
final BulkResponse response = action.fromXContent(parser);
90+
assertNotNull(response);
91+
assertEquals(10, response.getTook().millis());
92+
assertFalse(response.hasFailures());
93+
assertEquals(0, response.getItems().length);
94+
}
95+
}
96+
97+
@Test
98+
void test_fromXContent_withIngestTook() throws IOException {
99+
final String json = "{\"took\":5,\"ingest_took\":3,\"errors\":false,\"items\":[]}";
100+
try (final XContentParser parser =
101+
JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) {
102+
final BulkResponse response = action.fromXContent(parser);
103+
assertNotNull(response);
104+
assertEquals(5, response.getTook().millis());
105+
assertEquals(3, response.getIngestTookInMillis());
106+
}
107+
}
108+
109+
@Test
110+
void test_fromXContent_withIndexItem() throws IOException {
111+
final String json = "{\"took\":1,\"errors\":false,\"items\":[{\"index\":{\"_index\":\"test\",\"_id\":\"1\","
112+
+ "\"_version\":1,\"result\":\"created\",\"_shards\":{\"total\":2,\"successful\":1,\"failed\":0},"
113+
+ "\"_seq_no\":0,\"_primary_term\":1,\"status\":201}}]}";
114+
try (final XContentParser parser =
115+
JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) {
116+
final BulkResponse response = action.fromXContent(parser);
117+
assertNotNull(response);
118+
assertEquals(1, response.getItems().length);
119+
assertEquals("test", response.getItems()[0].getIndex());
120+
assertEquals("1", response.getItems()[0].getId());
121+
assertFalse(response.getItems()[0].isFailed());
122+
}
123+
}
124+
125+
@Test
126+
void test_fromXContent_withDeleteItem() throws IOException {
127+
final String json = "{\"took\":1,\"errors\":false,\"items\":[{\"delete\":{\"_index\":\"test\",\"_id\":\"1\","
128+
+ "\"_version\":2,\"result\":\"deleted\",\"_shards\":{\"total\":2,\"successful\":1,\"failed\":0},"
129+
+ "\"_seq_no\":1,\"_primary_term\":1,\"status\":200}}]}";
130+
try (final XContentParser parser =
131+
JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) {
132+
final BulkResponse response = action.fromXContent(parser);
133+
assertNotNull(response);
134+
assertEquals(1, response.getItems().length);
135+
assertFalse(response.getItems()[0].isFailed());
136+
}
137+
}
138+
139+
@Test
140+
void test_fromXContent_withFailedItem() throws IOException {
141+
final String json = "{\"took\":1,\"errors\":true,\"items\":[{\"index\":{\"_index\":\"test\",\"_id\":\"1\","
142+
+ "\"status\":400,\"error\":{\"type\":\"mapper_parsing_exception\"," + "\"reason\":\"failed to parse\"}}}]}";
143+
try (final XContentParser parser =
144+
JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) {
145+
final BulkResponse response = action.fromXContent(parser);
146+
assertNotNull(response);
147+
assertTrue(response.hasFailures());
148+
assertEquals(1, response.getItems().length);
149+
assertTrue(response.getItems()[0].isFailed());
150+
}
151+
}
152+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2012-2025 CodeLibs Project and the Others.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
13+
* either express or implied. See the License for the specific language
14+
* governing permissions and limitations under the License.
15+
*/
16+
package org.codelibs.fesen.client.action;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertNotNull;
20+
21+
import java.io.IOException;
22+
23+
import org.junit.jupiter.api.Test;
24+
import org.opensearch.action.delete.DeleteAction;
25+
import org.opensearch.action.delete.DeleteResponse;
26+
import org.opensearch.common.xcontent.json.JsonXContent;
27+
import org.opensearch.core.xcontent.DeprecationHandler;
28+
import org.opensearch.core.xcontent.NamedXContentRegistry;
29+
import org.opensearch.core.xcontent.XContentParser;
30+
31+
class HttpDeleteActionTest {
32+
33+
private final HttpDeleteAction action = new HttpDeleteAction(null, DeleteAction.INSTANCE);
34+
35+
@Test
36+
void test_fromXContent_deleted() throws IOException {
37+
final String json = "{\"_index\":\"test\",\"_id\":\"1\",\"_version\":2,\"result\":\"deleted\","
38+
+ "\"_shards\":{\"total\":2,\"successful\":1,\"failed\":0}," + "\"_seq_no\":1,\"_primary_term\":1}";
39+
try (final XContentParser parser =
40+
JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) {
41+
final DeleteResponse response = action.fromXContent(parser);
42+
assertNotNull(response);
43+
assertEquals("test", response.getIndex());
44+
assertEquals("1", response.getId());
45+
assertEquals(2, response.getVersion());
46+
assertEquals("deleted", response.getResult().getLowercase());
47+
}
48+
}
49+
50+
@Test
51+
void test_fromXContent_notFound() throws IOException {
52+
final String json = "{\"_index\":\"test\",\"_id\":\"1\",\"_version\":1,\"result\":\"not_found\","
53+
+ "\"_shards\":{\"total\":2,\"successful\":1,\"failed\":0}," + "\"_seq_no\":2,\"_primary_term\":1}";
54+
try (final XContentParser parser =
55+
JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, json)) {
56+
final DeleteResponse response = action.fromXContent(parser);
57+
assertNotNull(response);
58+
assertEquals("not_found", response.getResult().getLowercase());
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)