From 0936d7542a30242569e1a9e9ffe3df2dff33919f Mon Sep 17 00:00:00 2001 From: automation Date: Sat, 13 Sep 2025 21:28:02 +0100 Subject: [PATCH 1/3] fix: normalize base_url scheme and use shared Configuration --- docs/reference/configuration.md | 2 +- .../phoenixd/operation/AbstractOperation.java | 48 +++++++++---------- .../operation/BaseUrlNormalizationTest.java | 28 +++++++++++ .../xyz/tcheeric/phoenixd/test/TestUtils.java | 12 +++-- 4 files changed, 62 insertions(+), 28 deletions(-) create mode 100644 phoenixd-rest/src/test/java/xyz/tcheeric/phoenixd/operation/BaseUrlNormalizationTest.java diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 8293d1b..30ae281 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -20,7 +20,7 @@ Configuration can also be supplied through an `app.properties` file on the class |-----|--------|---------|-------| | `phoenixd.username` | String | empty | Username for basic authentication. | | `phoenixd.password` | String | empty | Password for basic authentication. Store securely. | -| `phoenixd.base_url` | URL | empty | Base address of the API endpoint. | +| `phoenixd.base_url` | URL | empty | Base address of the API endpoint. If no scheme is provided, `http://` is assumed. | | `phoenixd.timeout` | Integer (ms) | `5000` | HTTP request timeout. | | `phoenixd.webhook_secret` | String | empty | Optional secret used to validate webhook callbacks. Treat as a secret. | diff --git a/phoenixd-rest/src/main/java/xyz/tcheeric/phoenixd/operation/AbstractOperation.java b/phoenixd-rest/src/main/java/xyz/tcheeric/phoenixd/operation/AbstractOperation.java index ca8cbbd..8d5bdce 100644 --- a/phoenixd-rest/src/main/java/xyz/tcheeric/phoenixd/operation/AbstractOperation.java +++ b/phoenixd-rest/src/main/java/xyz/tcheeric/phoenixd/operation/AbstractOperation.java @@ -6,6 +6,7 @@ import xyz.tcheeric.phoenixd.operation.impl.PostOperation; import xyz.tcheeric.phoenixd.common.rest.Operation; import xyz.tcheeric.phoenixd.common.rest.Request; +import xyz.tcheeric.phoenixd.common.rest.util.Configuration; import java.io.IOException; import java.net.URI; @@ -19,7 +20,6 @@ import java.util.Base64; import java.util.List; import java.util.Map; -import java.util.Properties; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -32,34 +32,34 @@ public abstract class AbstractOperation implements Operation { private String requestData; public static final long DEFAULT_TIMEOUT = 5000L; - private static final String PREFIX = "phoenixd."; - private static final Properties CONFIG = loadConfig(); - - @SneakyThrows - private static Properties loadConfig() { - Properties props = new Properties(); - try (var stream = AbstractOperation.class.getResourceAsStream("/app.properties")) { - if (stream != null) { - props.load(stream); - } - } - return props; - } + private static final String PREFIX = "phoenixd"; + private static final Configuration CONFIG = new Configuration(PREFIX); private static String getProperty(String key) { - return CONFIG.getProperty(PREFIX + key); + return CONFIG.get(key); } private static long getLongProperty(String key, long defaultValue) { - String value = CONFIG.getProperty(PREFIX + key); - if (value == null) { - return defaultValue; + Long value = CONFIG.getLong(key, defaultValue); + return value != null ? value : defaultValue; + } + + private static String ensureScheme(String baseUrl) { + if (baseUrl == null || baseUrl.isBlank()) { + throw new IllegalArgumentException("phoenixd.base_url is not set"); } - try { - return Long.parseLong(value); - } catch (NumberFormatException e) { - return defaultValue; + String trimmed = baseUrl.trim(); + String lower = trimmed.toLowerCase(); + if (lower.startsWith("http://") || lower.startsWith("https://")) { + return trimmed; } + return "http://" + trimmed; + } + + private static URI buildUri(String baseUrl, String path) { + String normalizedBase = ensureScheme(baseUrl); + String normalizedPath = (path == null || path.isEmpty()) ? "/" : (path.startsWith("/") ? path : "/" + path); + return URI.create(normalizedBase).resolve(normalizedPath); } public AbstractOperation(@NonNull HttpRequest httpRequest) { @@ -80,7 +80,7 @@ public AbstractOperation(@NonNull String method, @NonNull String path, String re HttpRequest.BodyPublisher bodyPublisher = requestData == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(requestData); this.httpRequest = HttpRequest.newBuilder() - .uri(URI.create(baseUrl + path)) + .uri(buildUri(baseUrl, path)) .header("Authorization", "Basic " + encodedAuth) .timeout(Duration.ofMillis(timeout)) .method(method, bodyPublisher) @@ -107,7 +107,7 @@ public AbstractOperation(@NonNull String method, @NonNull String path, @NonNull } this.httpRequest = HttpRequest.newBuilder() - .uri(URI.create(baseUrl + resolvedPath)) + .uri(buildUri(baseUrl, resolvedPath)) .header("Authorization", "Basic " + encodedAuth) .timeout(Duration.ofMillis(timeout)) .method(method, bodyPublisher) diff --git a/phoenixd-rest/src/test/java/xyz/tcheeric/phoenixd/operation/BaseUrlNormalizationTest.java b/phoenixd-rest/src/test/java/xyz/tcheeric/phoenixd/operation/BaseUrlNormalizationTest.java new file mode 100644 index 0000000..a0a402d --- /dev/null +++ b/phoenixd-rest/src/test/java/xyz/tcheeric/phoenixd/operation/BaseUrlNormalizationTest.java @@ -0,0 +1,28 @@ +package xyz.tcheeric.phoenixd.operation; + +import org.junit.jupiter.api.Test; +import xyz.tcheeric.phoenixd.common.rest.util.Configuration; +import xyz.tcheeric.phoenixd.operation.impl.PostOperation; + +import java.lang.reflect.Field; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BaseUrlNormalizationTest { + + // Verifies base_url without scheme defaults to http + @Test + void baseUrlWithoutSchemeDefaultsToHttp() throws Exception { + Field configField = AbstractOperation.class.getDeclaredField("CONFIG"); + configField.setAccessible(true); + Configuration cfg = (Configuration) configField.get(null); + cfg.getProperties().setProperty("phoenixd.base_url", "localhost:9999"); + cfg.getProperties().setProperty("phoenixd.username", "user"); + cfg.getProperties().setProperty("phoenixd.password", "pass"); + + PostOperation op = new PostOperation("/items", "data"); + assertThat(op.getHttpRequest().uri().getScheme()).isEqualTo("http"); + assertThat(op.getHttpRequest().uri().toString()).isEqualTo("http://localhost:9999/items"); + } +} + diff --git a/phoenixd-test/src/test/java/xyz/tcheeric/phoenixd/test/TestUtils.java b/phoenixd-test/src/test/java/xyz/tcheeric/phoenixd/test/TestUtils.java index e250a35..6f7fc8b 100644 --- a/phoenixd-test/src/test/java/xyz/tcheeric/phoenixd/test/TestUtils.java +++ b/phoenixd-test/src/test/java/xyz/tcheeric/phoenixd/test/TestUtils.java @@ -1,9 +1,9 @@ package xyz.tcheeric.phoenixd.test; import xyz.tcheeric.phoenixd.operation.AbstractOperation; +import xyz.tcheeric.phoenixd.common.rest.util.Configuration; import java.lang.reflect.Field; -import java.util.Properties; public final class TestUtils { private static final String CONFIG_FIELD_NAME = "CONFIG"; @@ -16,8 +16,14 @@ public static void setBaseUrl(String baseUrl) { try { Field configField = AbstractOperation.class.getDeclaredField(CONFIG_FIELD_NAME); configField.setAccessible(true); - Properties props = (Properties) configField.get(null); - props.setProperty(PHOENIXD_BASE_URL_KEY, baseUrl); + Object cfg = configField.get(null); + if (cfg instanceof Configuration configuration) { + configuration.getProperties().setProperty(PHOENIXD_BASE_URL_KEY, baseUrl); + } else if (cfg instanceof java.util.Properties props) { + props.setProperty(PHOENIXD_BASE_URL_KEY, baseUrl); + } else { + throw new IllegalStateException("Unsupported CONFIG type: " + (cfg == null ? "null" : cfg.getClass())); + } } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } From 07c2b3d7d6cfb3997227119a06b87567a7027731 Mon Sep 17 00:00:00 2001 From: automation Date: Sat, 13 Sep 2025 21:28:07 +0100 Subject: [PATCH 2/3] build: bump version to 0.1.1 --- phoenixd-base/pom.xml | 2 +- phoenixd-mock/pom.xml | 2 +- phoenixd-model/pom.xml | 2 +- phoenixd-rest/pom.xml | 4 ++-- phoenixd-test/pom.xml | 2 +- pom.xml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/phoenixd-base/pom.xml b/phoenixd-base/pom.xml index 7e29dae..c34f125 100644 --- a/phoenixd-base/pom.xml +++ b/phoenixd-base/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric phoenixd-java - 0.1.0 + 0.1.1 phoenixd-base diff --git a/phoenixd-mock/pom.xml b/phoenixd-mock/pom.xml index f849e14..f88d178 100644 --- a/phoenixd-mock/pom.xml +++ b/phoenixd-mock/pom.xml @@ -6,7 +6,7 @@ xyz.tcheeric phoenixd-java - 0.1.0 + 0.1.1 phoenixd-mock diff --git a/phoenixd-model/pom.xml b/phoenixd-model/pom.xml index f4b3888..4770654 100644 --- a/phoenixd-model/pom.xml +++ b/phoenixd-model/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric phoenixd-java - 0.1.0 + 0.1.1 phoenixd-model diff --git a/phoenixd-rest/pom.xml b/phoenixd-rest/pom.xml index 8514809..0a4c38f 100644 --- a/phoenixd-rest/pom.xml +++ b/phoenixd-rest/pom.xml @@ -4,12 +4,12 @@ xyz.tcheeric phoenixd-java - 0.1.0 + 0.1.1 phoenixd-rest jar - 0.1.0 + 0.1.1 phoenixd-rest https://maven.apache.org diff --git a/phoenixd-test/pom.xml b/phoenixd-test/pom.xml index 3080763..94051b2 100644 --- a/phoenixd-test/pom.xml +++ b/phoenixd-test/pom.xml @@ -4,7 +4,7 @@ xyz.tcheeric phoenixd-java - 0.1.0 + 0.1.1 phoenixd-test diff --git a/pom.xml b/pom.xml index f5a01c4..20565db 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 xyz.tcheeric phoenixd-java - 0.1.0 + 0.1.1 pom From e5a5aeca7e3567ebdbd04ab1310b98772e4c2939 Mon Sep 17 00:00:00 2001 From: automation Date: Sat, 13 Sep 2025 21:30:41 +0100 Subject: [PATCH 3/3] docs: add CHANGELOG for 0.1.1 --- CHANGELOG.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3dade41 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,44 @@ +# Changelog + +## 0.1.1 — 2025-09-13 + +### Highlights +- Fixes “URI with undefined scheme” when `phoenixd.base_url` lacks an HTTP scheme by defaulting to `http://`. +- Centralizes configuration via shared `Configuration` util (env vars take precedence over `app.properties`). + +### Changes +- fix: normalize base_url scheme and use shared Configuration + - F:phoenixd-rest/src/main/java/xyz/tcheeric/phoenixd/operation/AbstractOperation.java + - F:phoenixd-rest/src/test/java/xyz/tcheeric/phoenixd/operation/BaseUrlNormalizationTest.java + - F:phoenixd-test/src/test/java/xyz/tcheeric/phoenixd/test/TestUtils.java +- docs: document scheme defaulting for `phoenixd.base_url` + - F:docs/reference/configuration.md +- build: bump version to 0.1.1 + - F:pom.xml + - F:phoenixd-rest/pom.xml + - F:phoenixd-test/pom.xml + - F:phoenixd-base/pom.xml + - F:phoenixd-model/pom.xml + - F:phoenixd-mock/pom.xml + +### Behavior Notes +- `phoenixd.base_url` without a scheme now resolves as `http://...`. +- If `phoenixd.base_url` is unset or blank, an `IllegalArgumentException` is thrown during request construction. + +### Configuration +- Env vars (preferred): `PHOENIXD_USERNAME`, `PHOENIXD_PASSWORD`, `PHOENIXD_BASE_URL`, `PHOENIXD_TIMEOUT`. +- Or `app.properties` on the classpath with `phoenixd.username`, `phoenixd.password`, `phoenixd.base_url`, `phoenixd.timeout`. + +### Testing +- Command: `mvn -q verify` +- Note: In restricted sandboxes, tests that open loopback sockets (MockWebServer) may fail with `SocketException: Operation not permitted`. In a normal dev/CI environment, the test suite is expected to pass. + +### API Changes +- None. + +### Security +- No new dependencies; no changes to security-sensitive logic. + +### Migration +- No breaking changes. If you relied on rejecting schemeless base URLs, be aware they now default to `http://`. +