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]");
+        }
+    }
+}

Reply via email to