This is an automated email from the ASF dual-hosted git repository. maciej pushed a commit to branch java-sdk-improvements in repository https://gitbox.apache.org/repos/asf/iggy.git
commit 10f2619ffb62c3a3595ee77429ad32a119ef3a0b Author: Maciej Modzelewski <[email protected]> AuthorDate: Thu Jan 29 09:28:21 2026 +0100 review updates --- .../src/main/java/org/apache/iggy/Iggy.java | 37 +---- .../src/main/java/org/apache/iggy/IggyVersion.java | 6 +- .../blocking/http/IggyHttpClientBuilder.java | 5 +- .../client/blocking/http/InternalHttpClient.java | 21 +-- .../iggy/client/blocking/http/UrlValidator.java | 60 ++++++++ .../exception/IggyAuthenticationException.java | 21 +++ .../iggy/exception/IggyAuthorizationException.java | 14 ++ .../iggy/exception/IggyConflictException.java | 20 +++ .../org/apache/iggy/exception/IggyErrorCode.java | 1 - .../exception/IggyResourceNotFoundException.java | 27 ++++ .../apache/iggy/exception/IggyServerException.java | 89 +++--------- .../iggy/exception/IggyValidationException.java | 31 ++++ .../src/test/java/org/apache/iggy/IggyTest.java | 10 +- .../test/java/org/apache/iggy/IggyVersionTest.java | 113 +++++++++++++++ .../client/async/AsyncClientIntegrationTest.java | 6 +- .../iggy/client/async/AsyncPollMessageTest.java | 5 +- .../async/tcp/AsyncIggyTcpClientBuilderTest.java | 21 ++- .../iggy/client/blocking/IntegrationTest.java | 1 + .../client/blocking/http/HttpClientFactory.java | 3 +- .../client/blocking/http/UrlValidatorTest.java | 90 ++++++++++++ .../blocking/tcp/IggyTcpClientBuilderTest.java | 24 ++-- .../iggy/client/blocking/tcp/TcpClientFactory.java | 3 +- .../apache/iggy/exception/IggyErrorCodeTest.java | 149 ++++++++++++++++++++ .../iggy/exception/IggyServerExceptionTest.java | 156 +++++++++++++++++++++ 24 files changed, 756 insertions(+), 157 deletions(-) diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/Iggy.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/Iggy.java index 492847fa8..e2d5cf743 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/Iggy.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/Iggy.java @@ -21,8 +21,6 @@ package org.apache.iggy; import org.apache.iggy.builder.HttpClientBuilder; import org.apache.iggy.builder.TcpClientBuilder; -import org.apache.iggy.client.blocking.http.IggyHttpClient; -import org.apache.iggy.client.blocking.tcp.IggyTcpClient; /** * Main entry point for creating Iggy clients. @@ -78,10 +76,6 @@ import org.apache.iggy.client.blocking.tcp.IggyTcpClient; */ public final class Iggy { - private static final String DEFAULT_HOST = "localhost"; - private static final int DEFAULT_TCP_PORT = 8090; - private static final int DEFAULT_HTTP_PORT = 3000; - private Iggy() {} /** @@ -91,7 +85,7 @@ public final class Iggy { * * @return a TCP client builder */ - public static TcpClientBuilder tcp() { + public static TcpClientBuilder tcpClientBuilder() { return new TcpClientBuilder(); } @@ -102,37 +96,10 @@ public final class Iggy { * * @return an HTTP client builder */ - public static HttpClientBuilder http() { + public static HttpClientBuilder httpClientBuilder() { return new HttpClientBuilder(); } - /** - * Creates a local blocking TCP client connected to localhost:8090. - * - * <p>This is a convenience method for local development and testing. - * Call {@code client.users().login(username, password)} to authenticate. - * - * @return a connected IggyTcpClient - */ - public static IggyTcpClient localTcp() { - IggyTcpClient client = - tcp().blocking().host(DEFAULT_HOST).port(DEFAULT_TCP_PORT).build(); - client.connect(); - return client; - } - - /** - * Creates a local blocking HTTP client connected to localhost:3000. - * - * <p>This is a convenience method for local development and testing. - * Call {@code client.users().login(username, password)} to authenticate. - * - * @return an IggyHttpClient - */ - public static IggyHttpClient localHttp() { - return http().blocking().host(DEFAULT_HOST).port(DEFAULT_HTTP_PORT).build(); - } - /** * Returns the SDK version string. * diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/IggyVersion.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/IggyVersion.java index a2c4a308d..59aeab832 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/IggyVersion.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/IggyVersion.java @@ -19,6 +19,9 @@ package org.apache.iggy; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.io.InputStream; import java.util.Properties; @@ -30,6 +33,7 @@ import java.util.Properties; */ public final class IggyVersion { + private static final Logger log = LoggerFactory.getLogger(IggyVersion.class); private static final String PROPERTIES_FILE = "/iggy-version.properties"; private static final String UNKNOWN = "unknown"; @@ -49,7 +53,7 @@ public final class IggyVersion { gitCommit = props.getProperty("gitCommit", UNKNOWN); } } catch (IOException e) { - // Use default values + log.warn("Failed to read version information from {}", PROPERTIES_FILE, e); } INSTANCE = new IggyVersion(version, buildTime, gitCommit); diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/IggyHttpClientBuilder.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/IggyHttpClientBuilder.java index 038957fba..28c000113 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/IggyHttpClientBuilder.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/IggyHttpClientBuilder.java @@ -63,6 +63,9 @@ import java.time.Duration; * @see IggyHttpClient#builder() */ public final class IggyHttpClientBuilder { + private static final String HTTPS_PROTOCOL = "https"; + private static final String HTTP_PROTOCOL = "http"; + private String url; private String host = "localhost"; private Integer port = IggyHttpClient.DEFAULT_HTTP_PORT; @@ -207,7 +210,7 @@ public final class IggyHttpClientBuilder { if (port == null || port <= 0) { throw new IggyInvalidArgumentException("Port must be a positive integer"); } - String protocol = enableTls ? "https" : "http"; + String protocol = enableTls ? HTTPS_PROTOCOL : HTTP_PROTOCOL; finalUrl = protocol + "://" + host + ":" + port; } return new IggyHttpClient(finalUrl, username, password, connectionTimeout, requestTimeout, tlsCertificate); diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/InternalHttpClient.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/InternalHttpClient.java index cd2edfafa..77e2a64ad 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/InternalHttpClient.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/InternalHttpClient.java @@ -19,7 +19,6 @@ package org.apache.iggy.client.blocking.http; -import org.apache.commons.lang3.StringUtils; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; @@ -34,7 +33,6 @@ import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; import org.apache.hc.core5.ssl.SSLContextBuilder; import org.apache.hc.core5.util.Timeout; import org.apache.iggy.exception.IggyConnectionException; -import org.apache.iggy.exception.IggyInvalidArgumentException; import org.apache.iggy.exception.IggyServerException; import org.apache.iggy.exception.IggyTlsException; import org.slf4j.Logger; @@ -66,20 +64,11 @@ final class InternalHttpClient implements Closeable { Optional<Duration> connectionTimeout, Optional<Duration> requestTimeout, Optional<File> tlsCertificate) { - validateUrl(url); + UrlValidator.validateHttpUrl(url); this.url = url; this.httpClient = createHttpClient(connectionTimeout, requestTimeout, tlsCertificate); } - private static void validateUrl(String url) { - if (StringUtils.isBlank(url)) { - throw new IggyInvalidArgumentException("URL cannot be null or empty"); - } - if (!url.startsWith("http://") && !url.startsWith("https://")) { - throw new IggyInvalidArgumentException("URL must start with http:// or https://"); - } - } - private static CloseableHttpClient createHttpClient( Optional<Duration> connectionTimeout, Optional<Duration> requestTimeout, Optional<File> tlsCertificate) { var connectionConfigBuilder = ConnectionConfig.custom(); @@ -205,10 +194,10 @@ final class InternalHttpClient implements Closeable { private void handleErrorResponse(ClassicHttpResponse response) throws IOException { if (!isSuccessful(response.getCode())) { var errorNode = objectMapper.readValue(response.getEntity().getContent(), ObjectNode.class); - String id = errorNode.has("id") ? errorNode.get("id").asText() : null; - String code = errorNode.has("code") ? errorNode.get("code").asText() : null; - String reason = errorNode.has("reason") ? errorNode.get("reason").asText() : null; - String field = errorNode.has("field") ? errorNode.get("field").asText() : null; + String id = errorNode.has("id") ? errorNode.get("id").asString() : null; + String code = errorNode.has("code") ? errorNode.get("code").asString() : null; + String reason = errorNode.has("reason") ? errorNode.get("reason").asString() : null; + String field = errorNode.has("field") ? errorNode.get("field").asString() : null; throw IggyServerException.fromHttpResponse(id, code, reason, field); } } diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/UrlValidator.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/UrlValidator.java new file mode 100644 index 000000000..557d1e5a4 --- /dev/null +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/UrlValidator.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.iggy.client.blocking.http; + +import org.apache.commons.lang3.StringUtils; +import org.apache.iggy.exception.IggyInvalidArgumentException; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; + +/** + * Utility class for validating HTTP URLs. + */ +final class UrlValidator { + + private static final String HTTP_SCHEME = "http"; + private static final String HTTPS_SCHEME = "https"; + + private UrlValidator() {} + + /** + * Validates that the given string is a valid HTTP or HTTPS URL. + * + * @param url the URL string to validate + * @throws IggyInvalidArgumentException if the URL is null, empty, malformed, + * or does not use http/https scheme + */ + static void validateHttpUrl(String url) { + if (StringUtils.isBlank(url)) { + throw new IggyInvalidArgumentException("URL cannot be null or empty"); + } + try { + var parsedUrl = new URI(url).toURL(); + String protocol = parsedUrl.getProtocol(); + if (protocol == null || (!protocol.equals(HTTP_SCHEME) && !protocol.equals(HTTPS_SCHEME))) { + throw new IggyInvalidArgumentException("URL must start with http:// or https://"); + } + } catch (URISyntaxException | MalformedURLException e) { + throw new IggyInvalidArgumentException("Invalid URL: " + e.getMessage()); + } + } +} diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyAuthenticationException.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyAuthenticationException.java index 0c4c8b51b..247513519 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyAuthenticationException.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyAuthenticationException.java @@ -19,7 +19,9 @@ package org.apache.iggy.exception; +import java.util.EnumSet; import java.util.Optional; +import java.util.Set; /** * Exception thrown when authentication fails. @@ -29,6 +31,15 @@ import java.util.Optional; */ public class IggyAuthenticationException extends IggyServerException { + private static final Set<IggyErrorCode> CODES = EnumSet.of( + IggyErrorCode.UNAUTHENTICATED, + IggyErrorCode.INVALID_CREDENTIALS, + IggyErrorCode.INVALID_USERNAME, + IggyErrorCode.INVALID_PASSWORD, + IggyErrorCode.INVALID_PAT_TOKEN, + IggyErrorCode.PASSWORD_DOES_NOT_MATCH, + IggyErrorCode.PASSWORD_HASH_INTERNAL_ERROR); + /** * Constructs a new IggyAuthenticationException. * @@ -46,4 +57,14 @@ public class IggyAuthenticationException extends IggyServerException { Optional<String> errorId) { super(errorCode, rawErrorCode, reason, field, errorId); } + + /** + * Returns whether the given error code should map to this exception type. + * + * @param code the error code to check + * @return true if this exception type handles the given error code + */ + public static boolean matches(IggyErrorCode code) { + return CODES.contains(code); + } } diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyAuthorizationException.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyAuthorizationException.java index b16cd7326..6490289ca 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyAuthorizationException.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyAuthorizationException.java @@ -19,7 +19,9 @@ package org.apache.iggy.exception; +import java.util.EnumSet; import java.util.Optional; +import java.util.Set; /** * Exception thrown when the user is not authorized to perform an operation. @@ -29,6 +31,8 @@ import java.util.Optional; */ public class IggyAuthorizationException extends IggyServerException { + private static final Set<IggyErrorCode> CODES = EnumSet.of(IggyErrorCode.UNAUTHORIZED); + /** * Constructs a new IggyAuthorizationException. * @@ -46,4 +50,14 @@ public class IggyAuthorizationException extends IggyServerException { Optional<String> errorId) { super(errorCode, rawErrorCode, reason, field, errorId); } + + /** + * Returns whether the given error code should map to this exception type. + * + * @param code the error code to check + * @return true if this exception type handles the given error code + */ + public static boolean matches(IggyErrorCode code) { + return CODES.contains(code); + } } diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyConflictException.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyConflictException.java index 82cd53c1c..7ff6b0733 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyConflictException.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyConflictException.java @@ -19,7 +19,9 @@ package org.apache.iggy.exception; +import java.util.EnumSet; import java.util.Optional; +import java.util.Set; /** * Exception thrown when a resource conflict occurs. @@ -29,6 +31,14 @@ import java.util.Optional; */ public class IggyConflictException extends IggyServerException { + private static final Set<IggyErrorCode> CODES = EnumSet.of( + IggyErrorCode.USER_ALREADY_EXISTS, + IggyErrorCode.CLIENT_ALREADY_EXISTS, + IggyErrorCode.STREAM_ALREADY_EXISTS, + IggyErrorCode.TOPIC_ALREADY_EXISTS, + IggyErrorCode.CONSUMER_GROUP_ALREADY_EXISTS, + IggyErrorCode.PAT_NAME_ALREADY_EXISTS); + /** * Constructs a new IggyConflictException. * @@ -46,4 +56,14 @@ public class IggyConflictException extends IggyServerException { Optional<String> errorId) { super(errorCode, rawErrorCode, reason, field, errorId); } + + /** + * Returns whether the given error code should map to this exception type. + * + * @param code the error code to check + * @return true if this exception type handles the given error code + */ + public static boolean matches(IggyErrorCode code) { + return CODES.contains(code); + } } diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyErrorCode.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyErrorCode.java index fe001bae1..426c4f5f7 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyErrorCode.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyErrorCode.java @@ -152,7 +152,6 @@ public enum IggyErrorCode { int numericCode = Integer.parseInt(code); return fromCode(numericCode); } catch (NumberFormatException e) { - // Try to match by name try { return valueOf(code.toUpperCase().replace(".", "_").replace(" ", "_")); } catch (IllegalArgumentException ex) { diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyResourceNotFoundException.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyResourceNotFoundException.java index 00058d5db..1f5ac25e1 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyResourceNotFoundException.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyResourceNotFoundException.java @@ -19,7 +19,9 @@ package org.apache.iggy.exception; +import java.util.EnumSet; import java.util.Optional; +import java.util.Set; /** * Exception thrown when a requested resource is not found on the server. @@ -29,6 +31,21 @@ import java.util.Optional; */ public class IggyResourceNotFoundException extends IggyServerException { + private static final Set<IggyErrorCode> CODES = EnumSet.of( + IggyErrorCode.RESOURCE_NOT_FOUND, + IggyErrorCode.CANNOT_LOAD_RESOURCE, + IggyErrorCode.STREAM_ID_NOT_FOUND, + IggyErrorCode.STREAM_NAME_NOT_FOUND, + IggyErrorCode.TOPIC_ID_NOT_FOUND, + IggyErrorCode.TOPIC_NAME_NOT_FOUND, + IggyErrorCode.PARTITION_NOT_FOUND, + IggyErrorCode.SEGMENT_NOT_FOUND, + IggyErrorCode.CLIENT_NOT_FOUND, + IggyErrorCode.CONSUMER_GROUP_ID_NOT_FOUND, + IggyErrorCode.CONSUMER_GROUP_NAME_NOT_FOUND, + IggyErrorCode.CONSUMER_GROUP_NOT_JOINED, + IggyErrorCode.MESSAGE_NOT_FOUND); + /** * Constructs a new IggyResourceNotFoundException. * @@ -46,4 +63,14 @@ public class IggyResourceNotFoundException extends IggyServerException { Optional<String> errorId) { super(errorCode, rawErrorCode, reason, field, errorId); } + + /** + * Returns whether the given error code should map to this exception type. + * + * @param code the error code to check + * @return true if this exception type handles the given error code + */ + public static boolean matches(IggyErrorCode code) { + return CODES.contains(code); + } } diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyServerException.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyServerException.java index ebfec159f..6678019ed 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyServerException.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyServerException.java @@ -22,9 +22,7 @@ package org.apache.iggy.exception; import org.apache.commons.lang3.StringUtils; import java.nio.charset.StandardCharsets; -import java.util.EnumSet; import java.util.Optional; -import java.util.Set; /** * Exception thrown when the server returns an error response. @@ -35,59 +33,6 @@ import java.util.Set; */ public class IggyServerException extends IggyException { - private static final Set<IggyErrorCode> NOT_FOUND_CODES = EnumSet.of( - IggyErrorCode.RESOURCE_NOT_FOUND, - IggyErrorCode.CANNOT_LOAD_RESOURCE, - IggyErrorCode.STREAM_ID_NOT_FOUND, - IggyErrorCode.STREAM_NAME_NOT_FOUND, - IggyErrorCode.TOPIC_ID_NOT_FOUND, - IggyErrorCode.TOPIC_NAME_NOT_FOUND, - IggyErrorCode.PARTITION_NOT_FOUND, - IggyErrorCode.SEGMENT_NOT_FOUND, - IggyErrorCode.CLIENT_NOT_FOUND, - IggyErrorCode.CONSUMER_GROUP_ID_NOT_FOUND, - IggyErrorCode.CONSUMER_GROUP_NAME_NOT_FOUND, - IggyErrorCode.CONSUMER_GROUP_NOT_JOINED, - IggyErrorCode.MESSAGE_NOT_FOUND); - - private static final Set<IggyErrorCode> AUTHENTICATION_CODES = EnumSet.of( - IggyErrorCode.UNAUTHENTICATED, - IggyErrorCode.INVALID_CREDENTIALS, - IggyErrorCode.INVALID_USERNAME, - IggyErrorCode.INVALID_PASSWORD, - IggyErrorCode.INVALID_PAT_TOKEN, - IggyErrorCode.PASSWORD_DOES_NOT_MATCH, - IggyErrorCode.PASSWORD_HASH_INTERNAL_ERROR); - - private static final Set<IggyErrorCode> AUTHORIZATION_CODES = EnumSet.of(IggyErrorCode.UNAUTHORIZED); - - private static final Set<IggyErrorCode> CONFLICT_CODES = EnumSet.of( - IggyErrorCode.USER_ALREADY_EXISTS, - IggyErrorCode.CLIENT_ALREADY_EXISTS, - IggyErrorCode.STREAM_ALREADY_EXISTS, - IggyErrorCode.TOPIC_ALREADY_EXISTS, - IggyErrorCode.CONSUMER_GROUP_ALREADY_EXISTS, - IggyErrorCode.PAT_NAME_ALREADY_EXISTS); - - private static final Set<IggyErrorCode> VALIDATION_CODES = EnumSet.of( - IggyErrorCode.INVALID_COMMAND, - IggyErrorCode.INVALID_FORMAT, - IggyErrorCode.FEATURE_UNAVAILABLE, - IggyErrorCode.CANNOT_PARSE_INT, - IggyErrorCode.CANNOT_PARSE_SLICE, - IggyErrorCode.CANNOT_PARSE_UTF8, - IggyErrorCode.INVALID_STREAM_NAME, - IggyErrorCode.CANNOT_CREATE_STREAM_DIRECTORY, - IggyErrorCode.INVALID_TOPIC_NAME, - IggyErrorCode.INVALID_REPLICATION_FACTOR, - IggyErrorCode.CANNOT_CREATE_TOPIC_DIRECTORY, - IggyErrorCode.CONSUMER_GROUP_MEMBER_NOT_FOUND, - IggyErrorCode.INVALID_CONSUMER_GROUP_NAME, - IggyErrorCode.TOO_MANY_MESSAGES, - IggyErrorCode.EMPTY_MESSAGES, - IggyErrorCode.TOO_BIG_MESSAGE, - IggyErrorCode.INVALID_MESSAGE_CHECKSUM); - private final IggyErrorCode errorCode; private final int rawErrorCode; private final String reason; @@ -123,7 +68,7 @@ public class IggyServerException extends IggyException { * @param rawErrorCode the raw numeric error code from the server */ public IggyServerException(int rawErrorCode) { - this(IggyErrorCode.fromCode(rawErrorCode), rawErrorCode, "Server error", Optional.empty(), Optional.empty()); + this(IggyErrorCode.fromCode(rawErrorCode), rawErrorCode, "", Optional.empty(), Optional.empty()); } /** @@ -201,35 +146,38 @@ public class IggyServerException extends IggyException { IggyErrorCode errorCode = IggyErrorCode.fromString(code); int rawCode = errorCode.getCode(); if (rawCode == -1) { - // Try parsing code as integer - try { - rawCode = Integer.parseInt(code); - } catch (NumberFormatException e) { - rawCode = -1; - } + rawCode = parseIntOrDefault(code, rawCode); } Optional<String> fieldOpt = Optional.ofNullable(StringUtils.stripToNull(field)); Optional<String> errorIdOpt = Optional.ofNullable(StringUtils.stripToNull(id)); - return createFromCode(rawCode, reason != null ? reason : "Server error", fieldOpt, errorIdOpt); + return createFromCode(rawCode, StringUtils.stripToEmpty(reason), fieldOpt, errorIdOpt); + } + + private static int parseIntOrDefault(String code, int rawCode) { + try { + return Integer.parseInt(code); + } catch (NumberFormatException e) { + return rawCode; + } } private static IggyServerException createFromCode( int code, String reason, Optional<String> field, Optional<String> errorId) { IggyErrorCode errorCode = IggyErrorCode.fromCode(code); - if (NOT_FOUND_CODES.contains(errorCode)) { + if (IggyResourceNotFoundException.matches(errorCode)) { return new IggyResourceNotFoundException(errorCode, code, reason, field, errorId); } - if (AUTHENTICATION_CODES.contains(errorCode)) { + if (IggyAuthenticationException.matches(errorCode)) { return new IggyAuthenticationException(errorCode, code, reason, field, errorId); } - if (AUTHORIZATION_CODES.contains(errorCode)) { + if (IggyAuthorizationException.matches(errorCode)) { return new IggyAuthorizationException(errorCode, code, reason, field, errorId); } - if (CONFLICT_CODES.contains(errorCode)) { + if (IggyConflictException.matches(errorCode)) { return new IggyConflictException(errorCode, code, reason, field, errorId); } - if (VALIDATION_CODES.contains(errorCode)) { + if (IggyValidationException.matches(errorCode)) { return new IggyValidationException(errorCode, code, reason, field, errorId); } @@ -247,7 +195,10 @@ public class IggyServerException extends IggyException { if (errorCode != IggyErrorCode.UNKNOWN) { sb.append(" (").append(errorCode.name()).append(")"); } - sb.append("]: ").append(reason); + sb.append("]"); + if (StringUtils.isNotBlank(reason)) { + sb.append(": ").append(reason); + } field.ifPresent(f -> sb.append(" (field: ").append(f).append(")")); errorId.ifPresent(id -> sb.append(" [errorId: ").append(id).append("]")); return sb.toString(); diff --git a/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyValidationException.java b/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyValidationException.java index bdd270d25..84e0b8eb4 100644 --- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyValidationException.java +++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/exception/IggyValidationException.java @@ -19,7 +19,9 @@ package org.apache.iggy.exception; +import java.util.EnumSet; import java.util.Optional; +import java.util.Set; /** * Exception thrown when input validation fails. @@ -29,6 +31,25 @@ import java.util.Optional; */ public class IggyValidationException extends IggyServerException { + private static final Set<IggyErrorCode> CODES = EnumSet.of( + IggyErrorCode.INVALID_COMMAND, + IggyErrorCode.INVALID_FORMAT, + IggyErrorCode.FEATURE_UNAVAILABLE, + IggyErrorCode.CANNOT_PARSE_INT, + IggyErrorCode.CANNOT_PARSE_SLICE, + IggyErrorCode.CANNOT_PARSE_UTF8, + IggyErrorCode.INVALID_STREAM_NAME, + IggyErrorCode.CANNOT_CREATE_STREAM_DIRECTORY, + IggyErrorCode.INVALID_TOPIC_NAME, + IggyErrorCode.INVALID_REPLICATION_FACTOR, + IggyErrorCode.CANNOT_CREATE_TOPIC_DIRECTORY, + IggyErrorCode.CONSUMER_GROUP_MEMBER_NOT_FOUND, + IggyErrorCode.INVALID_CONSUMER_GROUP_NAME, + IggyErrorCode.TOO_MANY_MESSAGES, + IggyErrorCode.EMPTY_MESSAGES, + IggyErrorCode.TOO_BIG_MESSAGE, + IggyErrorCode.INVALID_MESSAGE_CHECKSUM); + /** * Constructs a new IggyValidationException. * @@ -46,4 +67,14 @@ public class IggyValidationException extends IggyServerException { Optional<String> errorId) { super(errorCode, rawErrorCode, reason, field, errorId); } + + /** + * Returns whether the given error code should map to this exception type. + * + * @param code the error code to check + * @return true if this exception type handles the given error code + */ + public static boolean matches(IggyErrorCode code) { + return CODES.contains(code); + } } diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/IggyTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/IggyTest.java index dc5d2eecb..d08fb6547 100644 --- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/IggyTest.java +++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/IggyTest.java @@ -32,7 +32,7 @@ class IggyTest { @Test void tcpBuilderReturnsCorrectTypes() { - TcpClientBuilder tcpBuilder = Iggy.tcp(); + TcpClientBuilder tcpBuilder = Iggy.tcpClientBuilder(); assertThat(tcpBuilder).isNotNull(); IggyTcpClientBuilder blockingBuilder = tcpBuilder.blocking(); @@ -44,7 +44,7 @@ class IggyTest { @Test void httpBuilderReturnsCorrectTypes() { - HttpClientBuilder httpBuilder = Iggy.http(); + HttpClientBuilder httpBuilder = Iggy.httpClientBuilder(); assertThat(httpBuilder).isNotNull(); IggyHttpClientBuilder blockingBuilder = httpBuilder.blocking(); @@ -67,7 +67,7 @@ class IggyTest { @Test void tcpBlockingBuilderHasFluentApi() { - IggyTcpClientBuilder builder = Iggy.tcp().blocking(); + IggyTcpClientBuilder builder = Iggy.tcpClientBuilder().blocking(); // Verify fluent API returns same builder assertThat(builder.host("localhost")).isSameAs(builder); @@ -78,7 +78,7 @@ class IggyTest { @Test void tcpAsyncBuilderHasFluentApi() { - AsyncIggyTcpClientBuilder builder = Iggy.tcp().async(); + AsyncIggyTcpClientBuilder builder = Iggy.tcpClientBuilder().async(); // Verify fluent API returns same builder assertThat(builder.host("localhost")).isSameAs(builder); @@ -89,7 +89,7 @@ class IggyTest { @Test void httpBlockingBuilderHasFluentApi() { - IggyHttpClientBuilder builder = Iggy.http().blocking(); + IggyHttpClientBuilder builder = Iggy.httpClientBuilder().blocking(); // Verify fluent API returns same builder assertThat(builder.host("localhost")).isSameAs(builder); diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/IggyVersionTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/IggyVersionTest.java new file mode 100644 index 000000000..c90077472 --- /dev/null +++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/IggyVersionTest.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.iggy; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class IggyVersionTest { + + @Test + void getInstanceReturnsSingleton() { + IggyVersion instance1 = IggyVersion.getInstance(); + IggyVersion instance2 = IggyVersion.getInstance(); + + assertThat(instance1).isSameAs(instance2); + } + + @Test + void getVersionReturnsNonNullValue() { + IggyVersion version = IggyVersion.getInstance(); + + assertThat(version.getVersion()).isNotNull(); + assertThat(version.getVersion()).isNotEmpty(); + } + + @Test + void getBuildTimeReturnsNonNullValue() { + IggyVersion version = IggyVersion.getInstance(); + + assertThat(version.getBuildTime()).isNotNull(); + assertThat(version.getBuildTime()).isNotEmpty(); + } + + @Test + void getGitCommitReturnsNonNullValue() { + IggyVersion version = IggyVersion.getInstance(); + + assertThat(version.getGitCommit()).isNotNull(); + assertThat(version.getGitCommit()).isNotEmpty(); + } + + @Test + void getUserAgentHasCorrectFormat() { + IggyVersion version = IggyVersion.getInstance(); + + String userAgent = version.getUserAgent(); + + assertThat(userAgent).isNotNull(); + assertThat(userAgent).startsWith("iggy-java-sdk/"); + assertThat(userAgent).isEqualTo("iggy-java-sdk/" + version.getVersion()); + } + + @Test + void toStringContainsVersionInfo() { + IggyVersion version = IggyVersion.getInstance(); + + String str = version.toString(); + + assertThat(str).isNotNull(); + assertThat(str).startsWith("Iggy Java SDK "); + assertThat(str).contains(version.getVersion()); + } + + @Test + void toStringContainsBuildTimeWhenAvailable() { + IggyVersion version = IggyVersion.getInstance(); + + String str = version.toString(); + + if (!"unknown".equals(version.getBuildTime())) { + assertThat(str).contains("built: " + version.getBuildTime()); + } + } + + @Test + void toStringContainsGitCommitWhenBuildTimeAvailable() { + IggyVersion version = IggyVersion.getInstance(); + + String str = version.toString(); + + if (!"unknown".equals(version.getBuildTime()) && !"unknown".equals(version.getGitCommit())) { + assertThat(str).contains("commit: " + version.getGitCommit()); + } + } + + @Test + void versionValuesArePopulatedFromPropertiesFile() { + IggyVersion version = IggyVersion.getInstance(); + + // In a proper build, all values should be populated (not "unknown") + assertThat(version.getVersion()).isNotEqualTo("unknown"); + assertThat(version.getBuildTime()).isNotEqualTo("unknown"); + assertThat(version.getGitCommit()).isNotEqualTo("unknown"); + } +} diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/async/AsyncClientIntegrationTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/async/AsyncClientIntegrationTest.java index d90b010d0..c9d115997 100644 --- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/async/AsyncClientIntegrationTest.java +++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/async/AsyncClientIntegrationTest.java @@ -45,6 +45,8 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import static org.apache.iggy.client.blocking.IntegrationTest.LOCALHOST_IP; +import static org.apache.iggy.client.blocking.IntegrationTest.TCP_PORT; import static org.assertj.core.api.Assertions.assertThat; /** @@ -55,8 +57,6 @@ import static org.assertj.core.api.Assertions.assertThat; class AsyncClientIntegrationTest { private static final Logger log = LoggerFactory.getLogger(AsyncClientIntegrationTest.class); - private static final String HOST = "127.0.0.1"; - private static final int PORT = 8090; private static final String USERNAME = "iggy"; private static final String PASSWORD = "iggy"; @@ -69,7 +69,7 @@ class AsyncClientIntegrationTest { @BeforeAll public static void setup() throws Exception { log.info("Setting up async client for integration tests"); - client = new AsyncIggyTcpClient(HOST, PORT); + client = new AsyncIggyTcpClient(LOCALHOST_IP, TCP_PORT); // Connect and login client.connect() diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/async/AsyncPollMessageTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/async/AsyncPollMessageTest.java index f66aa5686..27c2b961e 100644 --- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/async/AsyncPollMessageTest.java +++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/async/AsyncPollMessageTest.java @@ -20,6 +20,7 @@ package org.apache.iggy.client.async; import org.apache.iggy.client.async.tcp.AsyncIggyTcpClient; +import org.apache.iggy.client.blocking.IntegrationTest; import org.apache.iggy.consumergroup.Consumer; import org.apache.iggy.identifier.StreamId; import org.apache.iggy.identifier.TopicId; @@ -79,7 +80,7 @@ public class AsyncPollMessageTest { // Ignore close errors } } - client = new AsyncIggyTcpClient("127.0.0.1", 8090); + client = new AsyncIggyTcpClient(IntegrationTest.LOCALHOST_IP, IntegrationTest.TCP_PORT); client.connect().get(5, TimeUnit.SECONDS); client.users().login("iggy", "iggy").get(5, TimeUnit.SECONDS); log.info("Client reconnected successfully"); @@ -101,7 +102,7 @@ public class AsyncPollMessageTest { log.info("Setting up async client for poll message tests"); // Initialize client - client = new AsyncIggyTcpClient("127.0.0.1", 8090); + client = new AsyncIggyTcpClient(IntegrationTest.LOCALHOST_IP, IntegrationTest.TCP_PORT); client.connect().get(5, TimeUnit.SECONDS); client.users().login("iggy", "iggy").get(5, TimeUnit.SECONDS); log.info("Successfully connected and logged in"); diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/async/tcp/AsyncIggyTcpClientBuilderTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/async/tcp/AsyncIggyTcpClientBuilderTest.java index 808ac0089..78ec99580 100644 --- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/async/tcp/AsyncIggyTcpClientBuilderTest.java +++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/async/tcp/AsyncIggyTcpClientBuilderTest.java @@ -26,6 +26,8 @@ import org.junit.jupiter.api.Test; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import static org.apache.iggy.client.blocking.IntegrationTest.LOCALHOST_IP; +import static org.apache.iggy.client.blocking.IntegrationTest.TCP_PORT; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -37,9 +39,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; */ class AsyncIggyTcpClientBuilderTest { - private static final String HOST = "127.0.0.1"; - private static final int PORT = 8090; - private AsyncIggyTcpClient client; @AfterEach @@ -52,7 +51,7 @@ class AsyncIggyTcpClientBuilderTest { @Test void shouldCreateClientWithBuilder() throws Exception { // Given: Builder with basic configuration - client = AsyncIggyTcpClient.builder().host(HOST).port(PORT).build(); + client = AsyncIggyTcpClient.builder().host(LOCALHOST_IP).port(TCP_PORT).build(); // When: Connect to server client.connect().get(5, TimeUnit.SECONDS); @@ -81,7 +80,7 @@ class AsyncIggyTcpClientBuilderTest { void shouldThrowExceptionForEmptyHost() { // Given: Builder with empty host AsyncIggyTcpClientBuilder builder = - AsyncIggyTcpClient.builder().host("").port(PORT); + AsyncIggyTcpClient.builder().host("").port(TCP_PORT); // When/Then: Building should throw IggyInvalidArgumentException assertThrows(IggyInvalidArgumentException.class, builder::build); @@ -91,7 +90,7 @@ class AsyncIggyTcpClientBuilderTest { void shouldThrowExceptionForNullHost() { // Given: Builder with null host AsyncIggyTcpClientBuilder builder = - AsyncIggyTcpClient.builder().host(null).port(PORT); + AsyncIggyTcpClient.builder().host(null).port(TCP_PORT); // When/Then: Building should throw IggyInvalidArgumentException assertThrows(IggyInvalidArgumentException.class, builder::build); @@ -101,7 +100,7 @@ class AsyncIggyTcpClientBuilderTest { void shouldThrowExceptionForInvalidPort() { // Given: Builder with invalid port AsyncIggyTcpClientBuilder builder = - AsyncIggyTcpClient.builder().host(HOST).port(-1); + AsyncIggyTcpClient.builder().host(LOCALHOST_IP).port(-1); // When/Then: Building should throw IggyInvalidArgumentException assertThrows(IggyInvalidArgumentException.class, builder::build); @@ -111,7 +110,7 @@ class AsyncIggyTcpClientBuilderTest { void shouldThrowExceptionForZeroPort() { // Given: Builder with zero port AsyncIggyTcpClientBuilder builder = - AsyncIggyTcpClient.builder().host(HOST).port(0); + AsyncIggyTcpClient.builder().host(LOCALHOST_IP).port(0); // When/Then: Building should throw IggyInvalidArgumentException assertThrows(IggyInvalidArgumentException.class, builder::build); @@ -120,7 +119,7 @@ class AsyncIggyTcpClientBuilderTest { @Test void shouldMaintainBackwardCompatibilityWithOldConstructor() throws Exception { // Given: Old constructor approach - client = new AsyncIggyTcpClient(HOST, PORT); + client = new AsyncIggyTcpClient(LOCALHOST_IP, TCP_PORT); // When: Connect to server client.connect().get(5, TimeUnit.SECONDS); @@ -132,7 +131,7 @@ class AsyncIggyTcpClientBuilderTest { @Test void shouldConnectAndPerformOperations() throws Exception { // Given: Client - client = AsyncIggyTcpClient.builder().host(HOST).port(PORT).build(); + client = AsyncIggyTcpClient.builder().host(LOCALHOST_IP).port(TCP_PORT).build(); // When: Connect client.connect().get(5, TimeUnit.SECONDS); @@ -148,7 +147,7 @@ class AsyncIggyTcpClientBuilderTest { @Test void shouldCloseConnectionGracefully() throws Exception { // Given: Connected client - client = AsyncIggyTcpClient.builder().host(HOST).port(PORT).build(); + client = AsyncIggyTcpClient.builder().host(LOCALHOST_IP).port(TCP_PORT).build(); client.connect().get(5, TimeUnit.SECONDS); // When: Close connection diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/IntegrationTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/IntegrationTest.java index 5d34e02a5..8420872d6 100644 --- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/IntegrationTest.java +++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/IntegrationTest.java @@ -42,6 +42,7 @@ import static org.apache.iggy.TestConstants.TOPIC_NAME; @Testcontainers public abstract class IntegrationTest { + public static final String LOCALHOST_IP = "127.0.0.1"; public static final int HTTP_PORT = 3000; public static final int TCP_PORT = 8090; protected static GenericContainer<?> iggyServer; diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/HttpClientFactory.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/HttpClientFactory.java index 6bdb1fd58..6f17c9ac6 100644 --- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/HttpClientFactory.java +++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/HttpClientFactory.java @@ -22,6 +22,7 @@ package org.apache.iggy.client.blocking.http; import org.testcontainers.containers.GenericContainer; import static org.apache.iggy.client.blocking.IntegrationTest.HTTP_PORT; +import static org.apache.iggy.client.blocking.IntegrationTest.LOCALHOST_IP; final class HttpClientFactory { @@ -30,7 +31,7 @@ final class HttpClientFactory { static IggyHttpClient create(GenericContainer<?> iggyServer) { if (iggyServer == null) { // Server is running externally - return new IggyHttpClient("http://127.0.0.1:" + HTTP_PORT); + return new IggyHttpClient("http://" + LOCALHOST_IP + ":" + HTTP_PORT); } String address = iggyServer.getHost(); Integer port = iggyServer.getMappedPort(HTTP_PORT); diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/UrlValidatorTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/UrlValidatorTest.java new file mode 100644 index 000000000..880628b3a --- /dev/null +++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/UrlValidatorTest.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.iggy.client.blocking.http; + +import org.apache.iggy.exception.IggyInvalidArgumentException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class UrlValidatorTest { + + @ParameterizedTest + @ValueSource( + strings = { + "http://localhost", + "http://localhost:3000", + "http://127.0.0.1:3000", + "http://example.com", + "http://example.com/path", + "http://example.com:8080/path?query=value", + "https://localhost", + "https://localhost:3000", + "https://127.0.0.1:3000", + "https://example.com", + "https://example.com/path", + "https://example.com:443/path?query=value" + }) + void shouldAcceptValidHttpUrls(String url) { + assertThatCode(() -> UrlValidator.validateHttpUrl(url)).doesNotThrowAnyException(); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {" ", "\t", "\n"}) + void shouldRejectNullOrBlankUrls(String url) { + assertThatThrownBy(() -> UrlValidator.validateHttpUrl(url)) + .isInstanceOf(IggyInvalidArgumentException.class) + .hasMessage("URL cannot be null or empty"); + } + + @ParameterizedTest + @ValueSource(strings = {"ftp://example.com", "file:///path/to/file"}) + void shouldRejectNonHttpSchemes(String url) { + assertThatThrownBy(() -> UrlValidator.validateHttpUrl(url)) + .isInstanceOf(IggyInvalidArgumentException.class) + .hasMessage("URL must start with http:// or https://"); + } + + @ParameterizedTest + @ValueSource(strings = {"ws://example.com", "wss://example.com", "mailto:[email protected]"}) + void shouldRejectUnsupportedSchemes(String url) { + // These schemes fail during URI.toURL() because Java has no protocol handler for them + assertThatThrownBy(() -> UrlValidator.validateHttpUrl(url)).isInstanceOf(IggyInvalidArgumentException.class); + } + + @Test + void shouldRejectUrlWithoutProtocol() { + assertThatThrownBy(() -> UrlValidator.validateHttpUrl("localhost:3000")) + .isInstanceOf(IggyInvalidArgumentException.class); + } + + @ParameterizedTest + @ValueSource(strings = {"http://", "https://", "http:// invalid", "http://[invalid"}) + void shouldRejectMalformedUrls(String url) { + assertThatThrownBy(() -> UrlValidator.validateHttpUrl(url)) + .isInstanceOf(IggyInvalidArgumentException.class) + .hasMessageStartingWith("Invalid URL:"); + } +} diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/tcp/IggyTcpClientBuilderTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/tcp/IggyTcpClientBuilderTest.java index ff226557a..77d39c1b4 100644 --- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/tcp/IggyTcpClientBuilderTest.java +++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/tcp/IggyTcpClientBuilderTest.java @@ -47,7 +47,7 @@ class IggyTcpClientBuilderTest extends IntegrationTest { void shouldCreateClientWithBuilder() { // Given: Builder with basic configuration and credentials IggyTcpClient client = IggyTcpClient.builder() - .host("127.0.0.1") + .host(LOCALHOST_IP) .port(TCP_PORT) .credentials("iggy", "iggy") .buildAndLogin(); @@ -61,7 +61,7 @@ class IggyTcpClientBuilderTest extends IntegrationTest { void shouldCreateClientWithCredentials() { // Given: Builder with credentials configured IggyTcpClient client = IggyTcpClient.builder() - .host("127.0.0.1") + .host(LOCALHOST_IP) .port(TCP_PORT) .credentials("iggy", "iggy") .buildAndLogin(); @@ -76,7 +76,7 @@ class IggyTcpClientBuilderTest extends IntegrationTest { void shouldCreateClientWithTimeoutConfiguration() { // Given: Builder with timeout configuration IggyTcpClient client = IggyTcpClient.builder() - .host("127.0.0.1") + .host(LOCALHOST_IP) .port(TCP_PORT) .connectionTimeout(Duration.ofSeconds(30)) .requestTimeout(Duration.ofSeconds(10)) @@ -92,7 +92,7 @@ class IggyTcpClientBuilderTest extends IntegrationTest { void shouldCreateClientWithConnectionPoolSize() { // Given: Builder with connection pool size IggyTcpClient client = IggyTcpClient.builder() - .host("127.0.0.1") + .host(LOCALHOST_IP) .port(TCP_PORT) .connectionPoolSize(10) .credentials("iggy", "iggy") @@ -107,7 +107,7 @@ class IggyTcpClientBuilderTest extends IntegrationTest { void shouldCreateClientWithRetryPolicy() { // Given: Builder with exponential backoff retry policy IggyTcpClient client = IggyTcpClient.builder() - .host("127.0.0.1") + .host(LOCALHOST_IP) .port(TCP_PORT) .retryPolicy(RetryPolicy.exponentialBackoff()) .credentials("iggy", "iggy") @@ -122,7 +122,7 @@ class IggyTcpClientBuilderTest extends IntegrationTest { void shouldCreateClientWithCustomRetryPolicy() { // Given: Builder with custom retry policy IggyTcpClient client = IggyTcpClient.builder() - .host("127.0.0.1") + .host(LOCALHOST_IP) .port(TCP_PORT) .retryPolicy(RetryPolicy.fixedDelay(5, Duration.ofMillis(500))) .credentials("iggy", "iggy") @@ -137,7 +137,7 @@ class IggyTcpClientBuilderTest extends IntegrationTest { void shouldCreateClientWithNoRetryPolicy() { // Given: Builder with no retry policy IggyTcpClient client = IggyTcpClient.builder() - .host("127.0.0.1") + .host(LOCALHOST_IP) .port(TCP_PORT) .retryPolicy(RetryPolicy.noRetry()) .credentials("iggy", "iggy") @@ -152,7 +152,7 @@ class IggyTcpClientBuilderTest extends IntegrationTest { void shouldCreateClientWithAllOptions() { // Given: Builder with all configuration options IggyTcpClient client = IggyTcpClient.builder() - .host("127.0.0.1") + .host(LOCALHOST_IP) .port(TCP_PORT) .connectionTimeout(Duration.ofSeconds(30)) .requestTimeout(Duration.ofSeconds(10)) @@ -198,7 +198,8 @@ class IggyTcpClientBuilderTest extends IntegrationTest { @Test void shouldThrowExceptionForInvalidPort() { // Given: Builder with invalid port - IggyTcpClientBuilder builder = IggyTcpClient.builder().host("127.0.0.1").port(-1); + IggyTcpClientBuilder builder = + IggyTcpClient.builder().host(LOCALHOST_IP).port(-1); // When/Then: Building should throw IggyInvalidArgumentException assertThrows(IggyInvalidArgumentException.class, builder::build); @@ -207,7 +208,8 @@ class IggyTcpClientBuilderTest extends IntegrationTest { @Test void shouldThrowExceptionForZeroPort() { // Given: Builder with zero port - IggyTcpClientBuilder builder = IggyTcpClient.builder().host("127.0.0.1").port(0); + IggyTcpClientBuilder builder = + IggyTcpClient.builder().host(LOCALHOST_IP).port(0); // When/Then: Building should throw IggyInvalidArgumentException assertThrows(IggyInvalidArgumentException.class, builder::build); @@ -216,7 +218,7 @@ class IggyTcpClientBuilderTest extends IntegrationTest { @Test void shouldWorkWithConstructorAndExplicitConnect() { // Given: Constructor approach with explicit connect - IggyTcpClient client = new IggyTcpClient("127.0.0.1", TCP_PORT); + IggyTcpClient client = new IggyTcpClient(LOCALHOST_IP, TCP_PORT); // When: Connect, login and perform operation client.connect(); diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/tcp/TcpClientFactory.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/tcp/TcpClientFactory.java index 897c8ef3d..d7be63057 100644 --- a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/tcp/TcpClientFactory.java +++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/tcp/TcpClientFactory.java @@ -21,6 +21,7 @@ package org.apache.iggy.client.blocking.tcp; import org.testcontainers.containers.GenericContainer; +import static org.apache.iggy.client.blocking.IntegrationTest.LOCALHOST_IP; import static org.apache.iggy.client.blocking.IntegrationTest.TCP_PORT; final class TcpClientFactory { @@ -31,7 +32,7 @@ final class TcpClientFactory { IggyTcpClient client; if (iggyServer == null) { // Server is running externally - client = new IggyTcpClient("127.0.0.1", TCP_PORT); + client = new IggyTcpClient(LOCALHOST_IP, TCP_PORT); } else { String address = iggyServer.getHost(); Integer port = iggyServer.getMappedPort(TCP_PORT); diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/exception/IggyErrorCodeTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/exception/IggyErrorCodeTest.java new file mode 100644 index 000000000..b4d5afb83 --- /dev/null +++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/exception/IggyErrorCodeTest.java @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.iggy.exception; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class IggyErrorCodeTest { + + @Nested + class FromString { + + @Test + void shouldReturnUnknownForNull() { + // when + IggyErrorCode result = IggyErrorCode.fromString(null); + + // then + assertThat(result).isEqualTo(IggyErrorCode.UNKNOWN); + } + + @Test + void shouldReturnUnknownForEmptyString() { + // when + IggyErrorCode result = IggyErrorCode.fromString(""); + + // then + assertThat(result).isEqualTo(IggyErrorCode.UNKNOWN); + } + + @Test + void shouldReturnUnknownForBlankString() { + // when + IggyErrorCode result = IggyErrorCode.fromString(" "); + + // then + assertThat(result).isEqualTo(IggyErrorCode.UNKNOWN); + } + + @Test + void shouldParseNumericCode() { + // when + IggyErrorCode result = IggyErrorCode.fromString("1009"); + + // then + assertThat(result).isEqualTo(IggyErrorCode.STREAM_ID_NOT_FOUND); + } + + @Test + void shouldReturnUnknownForUnknownNumericCode() { + // when + IggyErrorCode result = IggyErrorCode.fromString("99999"); + + // then + assertThat(result).isEqualTo(IggyErrorCode.UNKNOWN); + } + + @Test + void shouldParseUppercaseEnumName() { + // when + IggyErrorCode result = IggyErrorCode.fromString("STREAM_ID_NOT_FOUND"); + + // then + assertThat(result).isEqualTo(IggyErrorCode.STREAM_ID_NOT_FOUND); + } + + @Test + void shouldParseLowercaseEnumName() { + // when + IggyErrorCode result = IggyErrorCode.fromString("stream_id_not_found"); + + // then + assertThat(result).isEqualTo(IggyErrorCode.STREAM_ID_NOT_FOUND); + } + + @Test + void shouldParseMixedCaseEnumName() { + // when + IggyErrorCode result = IggyErrorCode.fromString("Stream_Id_Not_Found"); + + // then + assertThat(result).isEqualTo(IggyErrorCode.STREAM_ID_NOT_FOUND); + } + + @Test + void shouldParseNameWithDots() { + // when + IggyErrorCode result = IggyErrorCode.fromString("stream.id.not.found"); + + // then + assertThat(result).isEqualTo(IggyErrorCode.STREAM_ID_NOT_FOUND); + } + + @Test + void shouldParseNameWithSpaces() { + // when + IggyErrorCode result = IggyErrorCode.fromString("stream id not found"); + + // then + assertThat(result).isEqualTo(IggyErrorCode.STREAM_ID_NOT_FOUND); + } + + @Test + void shouldReturnUnknownForInvalidEnumName() { + // when + IggyErrorCode result = IggyErrorCode.fromString("not_a_valid_error_code"); + + // then + assertThat(result).isEqualTo(IggyErrorCode.UNKNOWN); + } + + @Test + void shouldParseSimpleErrorCode() { + // when + IggyErrorCode result = IggyErrorCode.fromString("error"); + + // then + assertThat(result).isEqualTo(IggyErrorCode.ERROR); + } + + @Test + void shouldParseCodeOne() { + // when + IggyErrorCode result = IggyErrorCode.fromString("1"); + + // then + assertThat(result).isEqualTo(IggyErrorCode.ERROR); + } + } +} diff --git a/foreign/java/java-sdk/src/test/java/org/apache/iggy/exception/IggyServerExceptionTest.java b/foreign/java/java-sdk/src/test/java/org/apache/iggy/exception/IggyServerExceptionTest.java new file mode 100644 index 000000000..bff91cb14 --- /dev/null +++ b/foreign/java/java-sdk/src/test/java/org/apache/iggy/exception/IggyServerExceptionTest.java @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.iggy.exception; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +class IggyServerExceptionTest { + + @Nested + class BuildMessage { + + @Test + void shouldBuildMessageWithKnownErrorCode() { + // given + IggyServerException exception = new IggyServerException( + IggyErrorCode.STREAM_ID_NOT_FOUND, 1009, "Stream not found", Optional.empty(), Optional.empty()); + + // then + assertThat(exception.getMessage()) + .isEqualTo("Server error [code=1009 (STREAM_ID_NOT_FOUND)]: Stream not found"); + } + + @Test + void shouldBuildMessageWithUnknownErrorCode() { + // given + IggyServerException exception = new IggyServerException( + IggyErrorCode.UNKNOWN, 99999, "Unknown error", Optional.empty(), Optional.empty()); + + // then + assertThat(exception.getMessage()).isEqualTo("Server error [code=99999]: Unknown error"); + } + + @Test + void shouldBuildMessageWithField() { + // given + IggyServerException exception = new IggyServerException( + IggyErrorCode.INVALID_STREAM_NAME, + 1013, + "Invalid stream name", + Optional.of("name"), + Optional.empty()); + + // then + assertThat(exception.getMessage()) + .isEqualTo("Server error [code=1013 (INVALID_STREAM_NAME)]: Invalid stream name (field: name)"); + } + + @Test + void shouldBuildMessageWithErrorId() { + // given + IggyServerException exception = new IggyServerException( + IggyErrorCode.UNAUTHENTICATED, 40, "Not authenticated", Optional.empty(), Optional.of("abc-123")); + + // then + assertThat(exception.getMessage()) + .isEqualTo("Server error [code=40 (UNAUTHENTICATED)]: Not authenticated [errorId: abc-123]"); + } + + @Test + void shouldBuildMessageWithFieldAndErrorId() { + // given + IggyServerException exception = new IggyServerException( + IggyErrorCode.INVALID_PASSWORD, + 44, + "Password too short", + Optional.of("password"), + Optional.of("xyz-789")); + + // then + assertThat(exception.getMessage()) + .isEqualTo( + "Server error [code=44 (INVALID_PASSWORD)]: Password too short (field: password) [errorId: xyz-789]"); + } + + @Test + void shouldBuildMessageWithRawCodeConstructor() { + // given + IggyServerException exception = new IggyServerException(1009); + + // then + assertThat(exception.getMessage()).isEqualTo("Server error [code=1009 (STREAM_ID_NOT_FOUND)]"); + } + + @Test + void shouldBuildMessageWithUnknownRawCode() { + // given + IggyServerException exception = new IggyServerException(88888); + + // then + assertThat(exception.getMessage()).isEqualTo("Server error [code=88888]"); + } + + @Test + void shouldBuildMessageWithEmptyReason() { + // given + IggyServerException exception = new IggyServerException( + IggyErrorCode.STREAM_ID_NOT_FOUND, 1009, "", Optional.empty(), Optional.empty()); + + // then + assertThat(exception.getMessage()).isEqualTo("Server error [code=1009 (STREAM_ID_NOT_FOUND)]"); + } + + @Test + void shouldBuildMessageWithBlankReason() { + // given + IggyServerException exception = new IggyServerException( + IggyErrorCode.TOPIC_ID_NOT_FOUND, 2010, " ", Optional.empty(), Optional.empty()); + + // then + assertThat(exception.getMessage()).isEqualTo("Server error [code=2010 (TOPIC_ID_NOT_FOUND)]"); + } + + @Test + void shouldBuildMessageWithEmptyReasonAndField() { + // given + IggyServerException exception = new IggyServerException( + IggyErrorCode.INVALID_STREAM_NAME, 1013, "", Optional.of("name"), Optional.empty()); + + // then + assertThat(exception.getMessage()) + .isEqualTo("Server error [code=1013 (INVALID_STREAM_NAME)] (field: name)"); + } + + @Test + void shouldBuildMessageWithEmptyReasonAndErrorId() { + // given + IggyServerException exception = new IggyServerException( + IggyErrorCode.UNAUTHENTICATED, 40, "", Optional.empty(), Optional.of("abc-123")); + + // then + assertThat(exception.getMessage()).isEqualTo("Server error [code=40 (UNAUTHENTICATED)] [errorId: abc-123]"); + } + } +}
