From a5981e40b3e8f41032c64f5fbc38291e6e107c51 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 16 Oct 2026 12:17:56 -0600 Subject: [PATCH] Standardize test packages --- .mex/context/http.md | 2 +- .mex/patterns/http-transport-change.md | 2 +- AGENTS.md | 2 + .../redis/RedisHandlerClientConfigTest.java | 23 - .../build/BuildInputPinningTest.java | 4 +- .../{ => tests}/core/CoreClasspathTest.java | 4 +- .../file/PrivateFilePermissionsTest.java | 4 +- .../ConfigurateConfigViewTest.java | 4 +- .../configurate/YamlConfigDocumentTest.java | 5 +- .../tests/packaging/FullArtifactTest.java | 2 +- .../HttpBackendLifecycleRecoveryTest.java | 19 +- .../http/HttpBackendSendLifecycleTest.java | 23 +- ...kendTransportConnectorConstructorTest.java | 10 +- .../HttpEnrollmentAuthorityOwnershipTest.java | 6 +- .../http/HttpEnrollmentPinTest.java | 35 +- ...HttpEnrollmentPublicationRecoveryTest.java | 19 +- .../http/HttpEnvelopeWireCodecTest.java | 13 +- .../http/HttpInboundJournalCapacityTest.java | 44 +- .../http/HttpInboundRetirementTest.java | 38 +- .../http/HttpInboundStoreAccess.java | 30 ++ .../http/HttpOutgoingOwnershipTest.java | 30 +- .../http/HttpOutgoingQueueCapacityTest.java | 22 +- .../http/HttpPendingEnrollmentExpiryTest.java | 8 +- .../servercomm/http/HttpProtocolAccess.java | 55 +++ .../http/HttpProxyLifecycleRecoveryTest.java | 43 +- .../http/HttpRenewalRateLimitTest.java | 21 +- .../HttpRevocationRetryIsolationTest.java | 6 +- .../servercomm/http/HttpSecretsAccess.java | 13 + .../tests/servercomm/http/HttpTestAccess.java | 135 ++++++ .../http/HttpTlsIdentityOwnershipTest.java | 10 +- .../http/HttpTransportRuntimeTest.java | 456 +++++++++--------- .../http/HttpTransportSecurityTest.java | 149 +++--- .../servercomm/http/PackagedTlsSmoke.java | 19 +- .../mqtt/MqttServerCommLifecycleTest.java | 7 +- .../mqtt/MqttServerCommRecoveryTest.java | 4 +- .../redis/RedisHandlerClientConfigTest.java | 27 ++ .../AbstractSqlTableInitializationTest.java | 7 +- .../sql/mysql/ConnectionManagerPoolTest.java | 6 +- .../sql/mysql/MariaDbDriverFallbackTest.java | 17 +- .../sql/mysql/MariaDbFallbackTlsTest.java | 31 +- .../sql/mysql/MySQLDisconnectTest.java | 5 +- .../sql/mysql/PostgreSqlTlsConfigTest.java | 5 +- .../sql/mysql/PostgreSqlTlsModeTest.java | 32 +- .../{ => tests}/sql/mysql/SharedSqlTest.java | 6 +- .../tests/sql/mysql/SqlTestAccess.java | 12 + .../tests/support/TestReflection.java | 58 +++ 46 files changed, 964 insertions(+), 509 deletions(-) delete mode 100644 SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/redis/RedisHandlerClientConfigTest.java rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/build/BuildInputPinningTest.java (97%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/core/CoreClasspathTest.java (96%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/file/PrivateFilePermissionsTest.java (98%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/file/config/configurate/ConfigurateConfigViewTest.java (97%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/file/config/configurate/YamlConfigDocumentTest.java (98%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpBackendLifecycleRecoveryTest.java (71%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpBackendSendLifecycleTest.java (89%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpBackendTransportConnectorConstructorTest.java (71%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpEnrollmentAuthorityOwnershipTest.java (97%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpEnrollmentPinTest.java (87%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpEnrollmentPublicationRecoveryTest.java (83%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpEnvelopeWireCodecTest.java (94%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpInboundJournalCapacityTest.java (70%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpInboundRetirementTest.java (78%) create mode 100644 SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpInboundStoreAccess.java rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpOutgoingOwnershipTest.java (74%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpOutgoingQueueCapacityTest.java (75%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpPendingEnrollmentExpiryTest.java (84%) create mode 100644 SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpProtocolAccess.java rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpProxyLifecycleRecoveryTest.java (78%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpRenewalRateLimitTest.java (84%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpRevocationRetryIsolationTest.java (92%) create mode 100644 SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpSecretsAccess.java create mode 100644 SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpTestAccess.java rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpTlsIdentityOwnershipTest.java (93%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpTransportRuntimeTest.java (80%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/HttpTransportSecurityTest.java (88%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/http/PackagedTlsSmoke.java (88%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/mqtt/MqttServerCommLifecycleTest.java (95%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/servercomm/mqtt/MqttServerCommRecoveryTest.java (97%) create mode 100644 SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/redis/RedisHandlerClientConfigTest.java rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/sql/mysql/AbstractSqlTableInitializationTest.java (96%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/sql/mysql/ConnectionManagerPoolTest.java (98%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/sql/mysql/MariaDbDriverFallbackTest.java (84%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/sql/mysql/MariaDbFallbackTlsTest.java (85%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/sql/mysql/MySQLDisconnectTest.java (95%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/sql/mysql/PostgreSqlTlsConfigTest.java (94%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/sql/mysql/PostgreSqlTlsModeTest.java (72%) rename SimpleAPI/src/test/java/com/bencodez/simpleapi/{ => tests}/sql/mysql/SharedSqlTest.java (91%) create mode 100644 SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/SqlTestAccess.java create mode 100644 SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/support/TestReflection.java diff --git a/.mex/context/http.md b/.mex/context/http.md index c4a25baf..d78cedb0 100644 --- a/.mex/context/http.md +++ b/.mex/context/http.md @@ -13,6 +13,6 @@ mex: # HTTP transport boundary -The HTTP transport uses pinned TLS and enrollment; certificate binding authenticates peers, while the envelopes are strictly parsed but not independently signed. Proxy-to-backend outgoing messages are durably queued, and both sides have durable inbound replay fences. Backend-to-proxy outgoing messages remain in memory with a bounded shutdown flush. A network response, queue write, and application acknowledgement are different events. A valid nonempty queue directory reports pending work; invalid or inaccessible queue structure throws `IOException`, which callers must handle conservatively. A restart-visible `RUNNING` callback is neither replayed nor acknowledged because its side effects are uncertain. Source: `SimpleAPI/src/main/java/com/bencodez/simpleapi/servercomm/http/`, `SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/`. +The HTTP transport uses pinned TLS and enrollment; certificate binding authenticates peers, while the envelopes are strictly parsed but not independently signed. Proxy-to-backend outgoing messages are durably queued, and both sides have durable inbound replay fences. Backend-to-proxy outgoing messages remain in memory with a bounded shutdown flush. A network response, queue write, and application acknowledgement are different events. A valid nonempty queue directory reports pending work; invalid or inaccessible queue structure throws `IOException`, which callers must handle conservatively. A restart-visible `RUNNING` callback is neither replayed nor acknowledged because its side effects are uncertain. Source: `SimpleAPI/src/main/java/com/bencodez/simpleapi/servercomm/http/`, `SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/`. Transport work must keep size bounds, shutdown behavior, and authentication checks together. Inspect current tests for the exact guarantee instead of inferring exactly-once behavior from a queue name. diff --git a/.mex/patterns/http-transport-change.md b/.mex/patterns/http-transport-change.md index aac832b3..55da632a 100644 --- a/.mex/patterns/http-transport-change.md +++ b/.mex/patterns/http-transport-change.md @@ -13,4 +13,4 @@ mex: # HTTP transport change -Map enqueue, persisted state, send, acknowledgement, restart recovery, and unreadable state before changing completion semantics. Keep TLS/enrollment and bounded execution checks in the same review. See `context/http.md` and current tests under `SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/`. +Map enqueue, persisted state, send, acknowledgement, restart recovery, and unreadable state before changing completion semantics. Keep TLS/enrollment and bounded execution checks in the same review. See `context/http.md` and current tests under `SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/`. diff --git a/AGENTS.md b/AGENTS.md index 3e2c53ed..5e77ef2b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,8 @@ mvn -B -f SimpleAPI/pom.xml test mvn -B -f SimpleAPI/pom.xml package ``` +All test classes and test-only helpers belong under `com.bencodez.simpleapi.tests` (or its subpackages), with matching paths under `SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/`. Keep production package names and visibility unchanged when organizing tests; use narrowly scoped test-only reflection access where an existing test needs implementation internals. Preserve test discovery and both full/shared artifact checks when moving tests. + Confirm current CI and POM settings before relying on these commands. Verify that the package invocation produced both the legacy/full artifact and every configured classifier, especially the `shared` JAR. Run `git diff --check` and confirm tests actually ran. ## Packaging boundaries diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/redis/RedisHandlerClientConfigTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/redis/RedisHandlerClientConfigTest.java deleted file mode 100644 index 14325f70..00000000 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/redis/RedisHandlerClientConfigTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.bencodez.simpleapi.servercomm.redis; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -class RedisHandlerClientConfigTest { - - @Test - void tlsCanBeEnabled() { - var config = RedisHandler.buildClientConfig("", "", 0, true); - - assertTrue(config.isSsl()); - assertEquals("HTTPS", config.getSslParameters().getEndpointIdentificationAlgorithm()); - } - - @Test - void tlsRemainsDisabledByDefault() { - assertFalse(RedisHandler.buildClientConfig("", "", 0, false).isSsl()); - } -} diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/build/BuildInputPinningTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/build/BuildInputPinningTest.java similarity index 97% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/build/BuildInputPinningTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/build/BuildInputPinningTest.java index a17d89f0..7d36fe6d 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/build/BuildInputPinningTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/build/BuildInputPinningTest.java @@ -1,4 +1,6 @@ -package com.bencodez.simpleapi.build; +package com.bencodez.simpleapi.tests.build; + + import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/core/CoreClasspathTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/core/CoreClasspathTest.java similarity index 96% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/core/CoreClasspathTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/core/CoreClasspathTest.java index c3769121..6e0bd018 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/core/CoreClasspathTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/core/CoreClasspathTest.java @@ -1,4 +1,6 @@ -package com.bencodez.simpleapi.core; +package com.bencodez.simpleapi.tests.core; + + import static org.junit.jupiter.api.Assertions.*; import java.net.URLClassLoader; diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/file/PrivateFilePermissionsTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/PrivateFilePermissionsTest.java similarity index 98% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/file/PrivateFilePermissionsTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/PrivateFilePermissionsTest.java index dcad178e..d15a7ee4 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/file/PrivateFilePermissionsTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/PrivateFilePermissionsTest.java @@ -1,4 +1,6 @@ -package com.bencodez.simpleapi.file; +package com.bencodez.simpleapi.tests.file; + +import com.bencodez.simpleapi.file.PrivateFilePermissions; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/file/config/configurate/ConfigurateConfigViewTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/config/configurate/ConfigurateConfigViewTest.java similarity index 97% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/file/config/configurate/ConfigurateConfigViewTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/config/configurate/ConfigurateConfigViewTest.java index 9e2cd9fd..02d99780 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/file/config/configurate/ConfigurateConfigViewTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/config/configurate/ConfigurateConfigViewTest.java @@ -1,4 +1,6 @@ -package com.bencodez.simpleapi.file.config.configurate; +package com.bencodez.simpleapi.tests.file.config.configurate; + +import com.bencodez.simpleapi.file.config.configurate.ConfigurateConfigView; import static org.junit.jupiter.api.Assertions.*; import java.util.ArrayList; diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/file/config/configurate/YamlConfigDocumentTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/config/configurate/YamlConfigDocumentTest.java similarity index 98% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/file/config/configurate/YamlConfigDocumentTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/config/configurate/YamlConfigDocumentTest.java index 5dd4a674..91882ea9 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/file/config/configurate/YamlConfigDocumentTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/file/config/configurate/YamlConfigDocumentTest.java @@ -1,4 +1,7 @@ -package com.bencodez.simpleapi.file.config.configurate; +package com.bencodez.simpleapi.tests.file.config.configurate; + +import com.bencodez.simpleapi.file.config.configurate.ConfigurateConfigView; +import com.bencodez.simpleapi.file.config.configurate.YamlConfigDocument; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.assumeTrue; diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/packaging/FullArtifactTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/packaging/FullArtifactTest.java index ba1c1793..c28ac297 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/packaging/FullArtifactTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/packaging/FullArtifactTest.java @@ -13,7 +13,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import com.bencodez.simpleapi.servercomm.http.PackagedTlsSmoke; +import com.bencodez.simpleapi.tests.servercomm.http.PackagedTlsSmoke; /** Package-phase checks: Maven's dependency classpath must not mask missing shaded classes. */ public class FullArtifactTest { diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpBackendLifecycleRecoveryTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpBackendLifecycleRecoveryTest.java similarity index 71% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpBackendLifecycleRecoveryTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpBackendLifecycleRecoveryTest.java index 05aaf7e4..509e23f6 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpBackendLifecycleRecoveryTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpBackendLifecycleRecoveryTest.java @@ -1,4 +1,9 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpBackendTransportConnector; +import com.bencodez.simpleapi.servercomm.http.HttpClientCredentialStore; +import com.bencodez.simpleapi.servercomm.http.HttpConnectionCode; +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -33,20 +38,20 @@ void callbackOwnedCloseCompletesItsJournalBeforeAsynchronousSealing() throws Exc callbackReturned.countDown(); })); String id = UUID.randomUUID().toString(); - connector.get().dispatch(new HttpTransportProtocol.Delivery(id, JsonEnvelope.builder("self-close").build())); + HttpTestAccess.dispatch(connector.get(), new HttpProtocolAccess.Delivery(id, JsonEnvelope.builder("self-close").build())); assertTrue(callbackReturned.await(2, TimeUnit.SECONDS), "callback-owned close must not drain its own worker"); connector.get().close(); long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2); - HttpInboundDeliveryStore.State state = null; + Object state = null; while (System.nanoTime() < deadline) { - state = HttpInboundDeliveryStore.inspect(clientDirectory).state(id); - if (state == HttpInboundDeliveryStore.State.COMPLETED) break; + state = HttpInboundStoreAccess.inspect(clientDirectory).state(id); + if (state == HttpInboundStoreAccess.State.COMPLETED) break; Thread.sleep(5); } - assertEquals(HttpInboundDeliveryStore.State.COMPLETED, state, + assertEquals(HttpInboundStoreAccess.State.COMPLETED, state, "the callback wrapper must mark completed before close seals the journal"); connector.get().start(); - assertFalse(connector.get().pollerAlive(), "a closed connector must not restart its poller"); + assertFalse(HttpTestAccess.pollerAlive(connector.get()), "a closed connector must not restart its poller"); connector.get().close(); } } diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpBackendSendLifecycleTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpBackendSendLifecycleTest.java similarity index 89% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpBackendSendLifecycleTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpBackendSendLifecycleTest.java index c248ff6f..ce66d1a3 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpBackendSendLifecycleTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpBackendSendLifecycleTest.java @@ -1,4 +1,11 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpBackendTransportConnector; +import com.bencodez.simpleapi.servercomm.http.HttpClientCredentialStore; +import com.bencodez.simpleapi.servercomm.http.HttpConnectionCode; +import com.bencodez.simpleapi.servercomm.http.HttpEnrollmentAuthority; +import com.bencodez.simpleapi.servercomm.http.HttpProxyTransportServer; +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -22,7 +29,7 @@ class HttpBackendSendLifecycleTest { void pausedSendCannotEnterAfterCloseAndFlushCutsOffNewSends() throws Exception { HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("proxy"), "localhost"); HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("authority")); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), identity, + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, ignored -> { })) { server.start(); HttpConnectionCode code = authority.createConnectionCode("lobby-1", server.endpoint("localhost"), Duration.ofMinutes(5)); @@ -44,7 +51,7 @@ void pausedSendCannotEnterAfterCloseAndFlushCutsOffNewSends() throws Exception { assertTrue(second.send(JsonEnvelope.builder("accepted-before-flush").build())); assertPausedSendIsRejectedByFlush(second); assertFalse(second.send(JsonEnvelope.builder("rejected-after-flush").build())); - assertEquals(0, second.queuedOutgoing()); + assertEquals(0, HttpTestAccess.queuedOutgoing(second)); second.start(); assertTrue(second.send(JsonEnvelope.builder("accepted-after-restart").build())); } @@ -55,7 +62,7 @@ void pausedSendCannotEnterAfterCloseAndFlushCutsOffNewSends() throws Exception { void restartedConnectorRequiresAResponseFromItsNewRun() throws Exception { HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("restart-proxy"), "localhost"); HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("restart-authority")); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), identity, + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, ignored -> { })) { server.start(); HttpConnectionCode code = authority.createConnectionCode("lobby-1", server.endpoint("localhost"), Duration.ofMinutes(5)); @@ -78,7 +85,7 @@ void restartedConnectorRequiresAResponseFromItsNewRun() throws Exception { void startOpensSendAdmissionBeforeThePollerCanRun() throws Exception { HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("startup-proxy"), "localhost"); HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("startup-authority")); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), identity, + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, ignored -> { })) { server.start(); HttpConnectionCode code = authority.createConnectionCode("lobby-1", server.endpoint("localhost"), Duration.ofMinutes(5)); @@ -108,7 +115,7 @@ void restartWaitsForThePreviousPollerToExit() throws Exception { HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("restart-proxy"), "localhost"); HttpConnectionCode code = new HttpConnectionCode("lobby-1", java.net.URI.create("https://localhost:8443/"), identity.serverCertificatePin(), identity.caCertificatePin(), java.time.Instant.now().plusSeconds(60), - HttpTransportSecrets.randomToken()); + HttpSecretsAccess.randomToken()); Path credentials = directory.resolve("restart-client"); HttpClientCredentialStore.saveEnrolled(credentials, code, identity.issueClientCertificate("lobby-1")); try (HttpBackendTransportConnector connector = new HttpBackendTransportConnector(credentials, ignored -> { })) { @@ -154,7 +161,7 @@ private static void assertPausedSendIsRejectedByClose(HttpBackendTransportConnec assertFalse(sender.isAlive()); assertFalse(closer.isAlive()); assertFalse(accepted.get()); - assertEquals(0, connector.queuedOutgoing()); + assertEquals(0, HttpTestAccess.queuedOutgoing(connector)); } private static void assertPausedSendIsRejectedByFlush(HttpBackendTransportConnector connector) throws Exception { @@ -182,7 +189,7 @@ private static void assertPausedSendIsRejectedByFlush(HttpBackendTransportConnec assertFalse(flusher.isAlive()); assertFalse(accepted.get()); assertTrue(flushed.get()); - assertEquals(0, connector.queuedOutgoing()); + assertEquals(0, HttpTestAccess.queuedOutgoing(connector)); } private static Field field(String name) throws Exception { diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpBackendTransportConnectorConstructorTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpBackendTransportConnectorConstructorTest.java similarity index 71% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpBackendTransportConnectorConstructorTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpBackendTransportConnectorConstructorTest.java index 5e61ad85..fb19b749 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpBackendTransportConnectorConstructorTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpBackendTransportConnectorConstructorTest.java @@ -1,4 +1,10 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpBackendTransportConnector; +import com.bencodez.simpleapi.servercomm.http.HttpConnectionCode; +import com.bencodez.simpleapi.servercomm.http.HttpEnrollmentAuthority; +import com.bencodez.simpleapi.servercomm.http.HttpProxyTransportServer; +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -15,7 +21,7 @@ class HttpBackendTransportConnectorConstructorTest { void invalidConnectionCodeDoesNotPreventImmediateValidRetry() throws Exception { HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("proxy"), "localhost"); HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("authority")); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, ignored -> { })) { server.start(); HttpConnectionCode code = authority.createConnectionCode("lobby-1", server.endpoint("localhost"), diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpEnrollmentAuthorityOwnershipTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpEnrollmentAuthorityOwnershipTest.java similarity index 97% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpEnrollmentAuthorityOwnershipTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpEnrollmentAuthorityOwnershipTest.java index ce701016..1772fc02 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpEnrollmentAuthorityOwnershipTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpEnrollmentAuthorityOwnershipTest.java @@ -1,4 +1,8 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpConnectionCode; +import com.bencodez.simpleapi.servercomm.http.HttpEnrollmentAuthority; +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpEnrollmentPinTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpEnrollmentPinTest.java similarity index 87% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpEnrollmentPinTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpEnrollmentPinTest.java index c1b55b23..fee9e40a 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpEnrollmentPinTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpEnrollmentPinTest.java @@ -1,4 +1,9 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpClientCredentialStore; +import com.bencodez.simpleapi.servercomm.http.HttpConnectionCode; +import com.bencodez.simpleapi.servercomm.http.HttpEnrollmentAuthority; +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -26,7 +31,7 @@ void enrollmentLifetimeRequiresAtLeastOneSecond() throws Exception { HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("proxy"), "localhost"); java.time.Clock clock = java.time.Clock.fixed(Instant.parse("2026-09-05T12:00:00.999Z"), java.time.ZoneOffset.UTC); - HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, clock); + HttpEnrollmentAuthority authority = HttpTestAccess.authority(identity, clock); URI endpoint = URI.create("https://localhost:8443/"); for (Duration lifetime : new Duration[] { Duration.ofNanos(1), Duration.ofMillis(999), Duration.ofSeconds(1).minusNanos(1), Duration.ZERO, Duration.ofSeconds(-1) }) { @@ -78,7 +83,7 @@ void acceptsBundleSignedByConnectionCodeCa() throws Exception { HttpClientCredentialStore.saveEnrolled(client, code, proxy.issueClientCertificate("lobby-1")); HttpClientCredentialStore.EnrolledClient enrolled = HttpClientCredentialStore.loadEnrolled(client); assertEquals(code.caCertificatePin(), enrolled.profile().caCertificatePin()); - assertEquals(code.caCertificatePin(), HttpTransportSecrets.certificatePin(enrolled.credential().caCertificate())); + assertEquals(code.caCertificatePin(), HttpSecretsAccess.certificatePin(enrolled.credential().caCertificate())); } @Test @@ -94,7 +99,7 @@ void rejectsPkcs12WithUnrelatedPrivateKeyDuringEnrollmentAndReplacement() throws HttpClientCredentialStore.saveEnrolled(client, code, original); HttpClientCredentialStore.EnrolledClient before = HttpClientCredentialStore.loadEnrolled(client); - assertThrows(java.io.IOException.class, () -> HttpClientCredentialStore.stageReplacement(client, mismatched)); + assertThrows(java.io.IOException.class, () -> HttpTestAccess.stageReplacement(client, mismatched)); HttpClientCredentialStore.EnrolledClient after = HttpClientCredentialStore.loadEnrolled(client); assertEquals(before.profile(), after.profile()); assertArrayEquals(before.credential().certificate().getEncoded(), after.credential().certificate().getEncoded()); @@ -110,7 +115,7 @@ void stageReplacementRejectsForeignCaAndPreservesActiveEnrollment() throws Excep HttpClientCredentialStore.saveEnrolled(client, code, originalCredential); HttpClientCredentialStore.EnrolledClient before = HttpClientCredentialStore.loadEnrolled(client); - assertThrows(java.io.IOException.class, () -> HttpClientCredentialStore.stageReplacement( + assertThrows(java.io.IOException.class, () -> HttpTestAccess.stageReplacement( client, foreign.issueClientCertificate("lobby-1"))); HttpClientCredentialStore.EnrolledClient after = HttpClientCredentialStore.loadEnrolled(client); assertEquals(before.profile(), after.profile()); @@ -123,25 +128,25 @@ void stageReplacementAcceptsCaRenewalWithSameAuthorityKey() throws Exception { java.time.Clock originalClock = java.time.Clock.fixed( now.minus(Duration.ofDays(9 * 365L + 30L)), java.time.ZoneOffset.UTC); Path proxyDirectory = directory.resolve("proxy"); - HttpTlsIdentity original = HttpTlsIdentity.loadOrCreate(proxyDirectory, "localhost", originalClock); - HttpTlsIdentity.IssuedClientCertificate originalCredential = original.issueClientCertificate("lobby-1", now); - String originalCaPin = HttpTransportSecrets.certificatePin(original.caCertificate()); + HttpTlsIdentity original = HttpTestAccess.identity(proxyDirectory, "localhost", originalClock); + HttpTlsIdentity.IssuedClientCertificate originalCredential = HttpTestAccess.issueClientCertificate(original, "lobby-1", now); + String originalCaPin = HttpSecretsAccess.certificatePin(original.caCertificate()); java.security.PublicKey originalCaKey = original.caCertificate().getPublicKey(); Path client = directory.resolve("client"); HttpConnectionCode code = new HttpConnectionCode("lobby-1", URI.create("https://localhost:8443/"), - HttpTransportSecrets.certificatePin(original.serverCertificate()), - HttpTransportSecrets.certificatePin(original.caCertificate()), now.plusSeconds(60), "A".repeat(43)); + HttpSecretsAccess.certificatePin(original.serverCertificate()), + HttpSecretsAccess.certificatePin(original.caCertificate()), now.plusSeconds(60), "A".repeat(43)); HttpClientCredentialStore.saveEnrolled(client, code, originalCredential); - HttpTlsIdentity renewed = HttpTlsIdentity.loadOrCreate(proxyDirectory, "localhost", + HttpTlsIdentity renewed = HttpTestAccess.identity(proxyDirectory, "localhost", java.time.Clock.fixed(now, java.time.ZoneOffset.UTC)); assertNotEquals(originalCaPin, renewed.caCertificatePin()); assertEquals(originalCaKey, renewed.caCertificate().getPublicKey()); - HttpClientCredentialStore.StagedCredential staged = HttpClientCredentialStore.stageReplacement( - client, renewed.issueClientCertificate("lobby-1", now)); + HttpTestAccess.StagedCredential staged = HttpTestAccess.stageReplacement( + client, HttpTestAccess.issueClientCertificate(renewed, "lobby-1", now)); assertEquals(renewed.caCertificatePin(), staged.profile().caCertificatePin()); - HttpClientCredentialStore.activateReplacement(client, staged); + HttpTestAccess.activateReplacement(client, staged); assertEquals(renewed.caCertificatePin(), HttpClientCredentialStore.loadProfile(client).caCertificatePin()); } @@ -182,7 +187,7 @@ void generatedConnectionCodesAreAlwaysAcceptedByTheParser() throws Exception { private HttpConnectionCode code(HttpTlsIdentity identity, String serverId) { return new HttpConnectionCode(serverId, URI.create("https://localhost:8443/"), identity.serverCertificatePin(), identity.caCertificatePin(), Instant.now().plusSeconds(300), - HttpTransportSecrets.randomToken()); + HttpSecretsAccess.randomToken()); } private HttpTlsIdentity.IssuedClientCertificate withUnrelatedPrivateKey( diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpEnrollmentPublicationRecoveryTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpEnrollmentPublicationRecoveryTest.java similarity index 83% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpEnrollmentPublicationRecoveryTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpEnrollmentPublicationRecoveryTest.java index 54bbd738..881ea32b 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpEnrollmentPublicationRecoveryTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpEnrollmentPublicationRecoveryTest.java @@ -1,4 +1,11 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpBackendTransportConnector; +import com.bencodez.simpleapi.servercomm.http.HttpClientCredentialStore; +import com.bencodez.simpleapi.servercomm.http.HttpConnectionCode; +import com.bencodez.simpleapi.servercomm.http.HttpEnrollmentAuthority; +import com.bencodez.simpleapi.servercomm.http.HttpProxyTransportServer; +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -28,7 +35,7 @@ void publishedInitialCredentialIsConfirmedWithoutReusingTheEnrollmentToken() thr Path credentials = directory.resolve("client").toAbsolutePath().normalize(); HttpConnectionCode code; HttpClientCredentialStore.ClientCredential published; - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, ignored -> { })) { server.start(); code = authority.createConnectionCode("lobby-1", server.endpoint("localhost"), Duration.ofMinutes(5)); @@ -59,7 +66,7 @@ void concurrentEnrollmentForOneCredentialDirectoryPublishesOneCertificate() thro HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("concurrent-authority")); Path credentials = directory.resolve("concurrent-client"); var workers = Executors.newFixedThreadPool(2); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, ignored -> { })) { server.start(); HttpConnectionCode code = authority.createConnectionCode("lobby-1", server.endpoint("localhost"), Duration.ofMinutes(5)); @@ -84,16 +91,16 @@ void enrollmentMonitorSerializesOnlyTheSameCredentialDirectory() throws Exceptio CountDownLatch sameDirectoryEntered = new CountDownLatch(1); var workers = Executors.newFixedThreadPool(3); try { - var first = workers.submit(() -> HttpClientCredentialStore.withEnrollmentLock(firstDirectory, () -> { + var first = workers.submit(() -> HttpTestAccess.withEnrollmentLock(firstDirectory, () -> { firstEntered.countDown(); releaseFirst.await(); return true; })); assertTrue(firstEntered.await(2, TimeUnit.SECONDS)); - var unrelated = workers.submit(() -> HttpClientCredentialStore.withEnrollmentLock(secondDirectory, () -> true)); + var unrelated = workers.submit(() -> HttpTestAccess.withEnrollmentLock(secondDirectory, () -> true)); assertTrue(unrelated.get(2, TimeUnit.SECONDS), "an unrelated credential root must not wait for another root's network operation"); - var sameDirectory = workers.submit(() -> HttpClientCredentialStore.withEnrollmentLock(firstDirectory, () -> { + var sameDirectory = workers.submit(() -> HttpTestAccess.withEnrollmentLock(firstDirectory, () -> { sameDirectoryEntered.countDown(); return true; })); diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpEnvelopeWireCodecTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpEnvelopeWireCodecTest.java similarity index 94% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpEnvelopeWireCodecTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpEnvelopeWireCodecTest.java index 1ce89ebd..6a3bbe38 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpEnvelopeWireCodecTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpEnvelopeWireCodecTest.java @@ -1,4 +1,11 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpBackendTransportConnector; +import com.bencodez.simpleapi.servercomm.http.HttpConnectionCode; +import com.bencodez.simpleapi.servercomm.http.HttpEnrollmentAuthority; +import com.bencodez.simpleapi.servercomm.http.HttpEnvelopeWireCodec; +import com.bencodez.simpleapi.servercomm.http.HttpProxyTransportServer; +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -92,14 +99,14 @@ void durableProxyQueueKeepsSemanticEnvelopeAcrossCodecChange() throws Exception try (var files = Files.walk(outgoing)) { persisted = files.filter(Files::isRegularFile).findFirst().orElseThrow(); } - HttpTransportProtocol.Delivery stored = HttpTransportProtocol.parseStoredDelivery(Files.readAllBytes(persisted)); + HttpProtocolAccess.Delivery stored = HttpProtocolAccess.parseStoredDelivery(Files.readAllBytes(persisted)); assertEquals("vote", stored.envelope().getSubChannel()); assertEquals("Example", stored.envelope().getFields().get("player")); assertFalse(stored.envelope().getFields().containsKey("wire-key")); HttpEnvelopeWireCodec replacement = codec("new"); try (HttpProxyTransportServer restarted = server(identity, authority, outgoing, replacement, ignored -> { })) { - HttpProxyTransportServer.Response response = restarted.backendStateForTest("backend-a") + HttpTestAccess.Response response = HttpTestAccess.backendStateForTest(restarted, "backend-a") .await("backend-a", UUID.randomUUID().toString(), 1L, java.util.List.of(), replacement); assertEquals(1, response.messages().size()); JsonEnvelope wire = response.messages().iterator().next().envelope(); diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpInboundJournalCapacityTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpInboundJournalCapacityTest.java similarity index 70% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpInboundJournalCapacityTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpInboundJournalCapacityTest.java index 47f7a2e9..f1729ec9 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpInboundJournalCapacityTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpInboundJournalCapacityTest.java @@ -1,4 +1,8 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpEnrollmentAuthority; +import com.bencodez.simpleapi.servercomm.http.HttpProxyTransportServer; +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -26,18 +30,18 @@ void restoresRunningInboundJournalsAndEnforcesCapacityAcrossRestart() throws Exc HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("authority")); seedRunningJournals(outgoing, 128); - try (HttpProxyTransportServer proxy = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), - identity, authority, outgoing, ignored -> { })) { - assertEquals(128, proxy.backendCountForTest(), "validated journals must restore backend state"); - assertNotNull(proxy.backendStateForTest("server-0")); - assertThrows(java.io.IOException.class, () -> proxy.backendStateForTest("server-128"), + try (HttpProxyTransportServer proxy = new HttpProxyTransportServer( + new InetSocketAddress("localhost", 0), identity, authority, outgoing, ignored -> { })) { + assertEquals(128, HttpTestAccess.backendCountForTest(proxy), "validated journals must restore backend state"); + assertNotNull(HttpTestAccess.backendStateForTest(proxy, "server-0")); + assertThrows(java.io.IOException.class, () -> HttpTestAccess.backendStateForTest(proxy, "server-128"), "a 129th backend must be rejected while durable journals occupy the cap"); } - try (HttpProxyTransportServer restarted = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), - identity, authority, outgoing, ignored -> { })) { - assertEquals(128, restarted.backendCountForTest(), "restart must retain the restored backend count"); - assertThrows(java.io.IOException.class, () -> restarted.backendStateForTest("server-128"), + try (HttpProxyTransportServer restarted = new HttpProxyTransportServer( + new InetSocketAddress("localhost", 0), identity, authority, outgoing, ignored -> { })) { + assertEquals(128, HttpTestAccess.backendCountForTest(restarted), "restart must retain the restored backend count"); + assertThrows(java.io.IOException.class, () -> HttpTestAccess.backendStateForTest(restarted, "server-128"), "the cap must remain enforced after restart"); } } @@ -50,11 +54,11 @@ void prunesEmptySafeJournalsAndAllowsAReplacementBackend() throws Exception { HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("authority")); seedEmptyJournals(outgoing, 128); - try (HttpProxyTransportServer proxy = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), - identity, authority, outgoing, ignored -> { })) { - assertEquals(0, proxy.backendCountForTest(), "empty journals are safe to prune at startup"); - assertNotNull(proxy.backendStateForTest("replacement")); - assertEquals(1, proxy.backendCountForTest()); + try (HttpProxyTransportServer proxy = new HttpProxyTransportServer( + new InetSocketAddress("localhost", 0), identity, authority, outgoing, ignored -> { })) { + assertEquals(0, HttpTestAccess.backendCountForTest(proxy), "empty journals are safe to prune at startup"); + assertNotNull(HttpTestAccess.backendStateForTest(proxy, "replacement")); + assertEquals(1, HttpTestAccess.backendCountForTest(proxy)); assertFalse(Files.exists(outgoing.getParent().resolve("outgoing-incoming").resolve("server-0"))); } } @@ -68,17 +72,17 @@ void quarantineOnlyBackendIsIncludedInTheCombinedStartupCapacity() throws Except seedRunningJournals(outgoing, 128); String serverId = "server-128"; String deliveryId = UUID.nameUUIDFromBytes(serverId.getBytes(StandardCharsets.UTF_8)).toString(); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(deliveryId, + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(deliveryId, JsonEnvelope.builder("quarantined-retry").build()); Path serverDirectory = Files.createDirectory(outgoing.resolve(serverId)); Files.write(serverDirectory.resolve(".pending-" + deliveryId + ".json"), - HttpTransportProtocol.storedDelivery(delivery)); + HttpProtocolAccess.storedDelivery(delivery)); assertThrows(java.io.IOException.class, () -> new HttpProxyTransportServer( new InetSocketAddress("localhost", 0), identity, authority, outgoing, ignored -> { }), "129 distinct durable backend states must be rejected during startup rather than blocking a later retry"); - try (HttpProxyTransportServer.DurableOutgoingQueue queue = new HttpProxyTransportServer.DurableOutgoingQueue( + try (HttpTestAccess.DurableOutgoingQueue queue = new HttpTestAccess.DurableOutgoingQueue( outgoing, com.bencodez.simpleapi.file.DurableFiles::forceDirectory)) { var loaded = queue.load(); assertTrue(loaded.containsKey(serverId), "quarantine-only state must reserve its backend identity"); @@ -91,7 +95,7 @@ private static void seedRunningJournals(Path outgoing, int count) throws Excepti Files.createDirectory(incoming); for (int index = 0; index < count; index++) { String serverId = "server-" + index; - HttpInboundDeliveryStore store = HttpInboundDeliveryStore.open(incoming, serverId); + HttpInboundStoreAccess store = HttpInboundStoreAccess.open(incoming, serverId); String deliveryId = UUID.nameUUIDFromBytes(serverId.getBytes(StandardCharsets.UTF_8)).toString(); store.reserve(deliveryId); store.markRunning(deliveryId); @@ -103,7 +107,7 @@ private static void seedEmptyJournals(Path outgoing, int count) throws Exception Path incoming = outgoing.getParent().resolve(outgoing.getFileName() + "-incoming"); Files.createDirectory(incoming); for (int index = 0; index < count; index++) { - HttpInboundDeliveryStore store = HttpInboundDeliveryStore.open(incoming, "server-" + index); + HttpInboundStoreAccess store = HttpInboundStoreAccess.open(incoming, "server-" + index); store.seal(); } } diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpInboundRetirementTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpInboundRetirementTest.java similarity index 78% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpInboundRetirementTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpInboundRetirementTest.java index cf3d01a0..17da247c 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpInboundRetirementTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpInboundRetirementTest.java @@ -1,4 +1,6 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -23,7 +25,7 @@ class HttpInboundRetirementTest { void failedRetirementKeepsOwnershipAndRestoresRootOnNextWrite() throws Exception { Path parent = directory.resolve("incoming"); Files.createDirectory(parent); - HttpInboundDeliveryStore store = HttpInboundDeliveryStore.open(parent, "lobby-1"); + HttpInboundStoreAccess store = HttpInboundStoreAccess.open(parent, "lobby-1"); AtomicBoolean fail = new AtomicBoolean(true); try (var forces = org.mockito.Mockito.mockStatic(com.bencodez.simpleapi.file.DurableFiles.class, org.mockito.Mockito.CALLS_REAL_METHODS)) { @@ -32,11 +34,11 @@ void failedRetirementKeepsOwnershipAndRestoresRootOnNextWrite() throws Exception return call.callRealMethod(); }); assertThrows(IOException.class, store::sealAndDeleteIfEmpty); - assertThrows(IOException.class, () -> HttpInboundDeliveryStore.open(parent, "lobby-1")); + assertThrows(IOException.class, () -> HttpInboundStoreAccess.open(parent, "lobby-1")); String id = UUID.randomUUID().toString(); store.reserve(id); store.markRunning(id); - assertEquals(HttpInboundDeliveryStore.State.RUNNING, store.state(id)); + assertEquals(HttpInboundStoreAccess.State.RUNNING, store.state(id)); } store.seal(); } @@ -45,7 +47,7 @@ void failedRetirementKeepsOwnershipAndRestoresRootOnNextWrite() throws Exception void failedRetirementCanRetryAfterDeleteFailure() throws Exception { Path parent = directory.resolve("incoming"); Files.createDirectory(parent); - HttpInboundDeliveryStore store = HttpInboundDeliveryStore.open(parent, "lobby-1"); + HttpInboundStoreAccess store = HttpInboundStoreAccess.open(parent, "lobby-1"); Path root = parent.resolve("lobby-1"); AtomicBoolean fail = new AtomicBoolean(true); try (var files = org.mockito.Mockito.mockStatic(Files.class, org.mockito.Mockito.CALLS_REAL_METHODS)) { @@ -56,10 +58,10 @@ void failedRetirementCanRetryAfterDeleteFailure() throws Exception { // CALLS_REAL_METHODS executes the unstubbed delete while registering it. Files.createDirectory(root); assertThrows(IOException.class, store::sealAndDeleteIfEmpty); - assertThrows(IOException.class, () -> HttpInboundDeliveryStore.open(parent, "lobby-1")); + assertThrows(IOException.class, () -> HttpInboundStoreAccess.open(parent, "lobby-1")); store.sealAndDeleteIfEmpty(); } - HttpInboundDeliveryStore successor = HttpInboundDeliveryStore.open(parent, "lobby-1"); + HttpInboundStoreAccess successor = HttpInboundStoreAccess.open(parent, "lobby-1"); successor.seal(); } @@ -68,7 +70,7 @@ void successfulRetirementReclaimsPerJournalOwnershipSidecars() throws Exception Path parent = directory.resolve("incoming"); Files.createDirectory(parent); for (int index = 0; index < 200; index++) { - HttpInboundDeliveryStore store = HttpInboundDeliveryStore.open(parent, "backend-" + index); + HttpInboundStoreAccess store = HttpInboundStoreAccess.open(parent, "backend-" + index); store.sealAndDeleteIfEmpty(); } @@ -76,14 +78,14 @@ void successfulRetirementReclaimsPerJournalOwnershipSidecars() throws Exception assertEquals(java.util.List.of(".http-inbound-owners.lock"), files.map(path -> path.getFileName().toString()).sorted().toList()); } - assertTrue(HttpInboundDeliveryStore.discover(parent).isEmpty()); + assertTrue(HttpInboundStoreAccess.discover(parent).isEmpty()); } @Test void sidecarCleanupFailureStillCompletesRetirement() throws Exception { Path parent = directory.resolve("incoming"); Files.createDirectory(parent); - HttpInboundDeliveryStore store = HttpInboundDeliveryStore.open(parent, "lobby-1"); + HttpInboundStoreAccess store = HttpInboundStoreAccess.open(parent, "lobby-1"); Path sidecar = parent.resolve(".http-inbound-owner-lobby-1.lock"); try (var files = org.mockito.Mockito.mockStatic(com.bencodez.simpleapi.file.DurableFiles.class, org.mockito.Mockito.CALLS_REAL_METHODS)) { @@ -92,7 +94,7 @@ void sidecarCleanupFailureStillCompletesRetirement() throws Exception { store.sealAndDeleteIfEmpty(); } - HttpInboundDeliveryStore successor = HttpInboundDeliveryStore.open(parent, "lobby-1"); + HttpInboundStoreAccess successor = HttpInboundStoreAccess.open(parent, "lobby-1"); successor.sealAndDeleteIfEmpty(); } @@ -100,9 +102,9 @@ void sidecarCleanupFailureStillCompletesRetirement() throws Exception { void retirementCompletesWhenOwnershipReleaseOrCloseFails() throws Exception { Path parent = directory.resolve("incoming"); Files.createDirectory(parent); - HttpInboundDeliveryStore store = HttpInboundDeliveryStore.open(parent, "lobby-1"); - FileLock originalLock = (FileLock) field("ownershipLock").get(store); - FileChannel originalChannel = (FileChannel) field("ownershipChannel").get(store); + HttpInboundStoreAccess store = HttpInboundStoreAccess.open(parent, "lobby-1"); + FileLock originalLock = (FileLock) field("ownershipLock").get(store.target()); + FileChannel originalChannel = (FileChannel) field("ownershipChannel").get(store.target()); originalLock.release(); originalChannel.close(); FileLock failingLock = org.mockito.Mockito.mock(FileLock.class); @@ -110,17 +112,17 @@ void retirementCompletesWhenOwnershipReleaseOrCloseFails() throws Exception { org.mockito.Mockito.when(failingLock.isValid()).thenReturn(true); org.mockito.Mockito.doThrow(new IOException("injected release failure")).when(failingLock).release(); org.mockito.Mockito.doThrow(new IOException("injected close failure")).when(failingChannel).close(); - field("ownershipLock").set(store, failingLock); - field("ownershipChannel").set(store, failingChannel); + field("ownershipLock").set(store.target(), failingLock); + field("ownershipChannel").set(store.target(), failingChannel); assertDoesNotThrow(store::sealAndDeleteIfEmpty); assertThrows(IOException.class, () -> store.reserve(UUID.randomUUID().toString())); - HttpInboundDeliveryStore successor = HttpInboundDeliveryStore.open(parent, "lobby-1"); + HttpInboundStoreAccess successor = HttpInboundStoreAccess.open(parent, "lobby-1"); successor.sealAndDeleteIfEmpty(); } private static Field field(String name) throws Exception { - Field field = HttpInboundDeliveryStore.class.getDeclaredField(name); + Field field = HttpInboundStoreAccess.TYPE.getDeclaredField(name); field.setAccessible(true); return field; } diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpInboundStoreAccess.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpInboundStoreAccess.java new file mode 100644 index 00000000..3e5b4584 --- /dev/null +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpInboundStoreAccess.java @@ -0,0 +1,30 @@ +package com.bencodez.simpleapi.tests.servercomm.http; + +import static com.bencodez.simpleapi.tests.support.TestReflection.*; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Set; + +/** Test-only view of the original durable inbound store. */ +final class HttpInboundStoreAccess extends HttpProtocolAccess.View { + static final Class TYPE = type("com.bencodez.simpleapi.servercomm.http.HttpInboundDeliveryStore"); + HttpInboundStoreAccess(Object target) { super(target); } + HttpInboundStoreAccess(Path directory) throws IOException { super(construct(TYPE, directory)); } + static final class State { + private static final Class TYPE = type("com.bencodez.simpleapi.servercomm.http.HttpInboundDeliveryStore$State"); + static final Object RESERVED = field(TYPE, null, "RESERVED"); + static final Object RUNNING = field(TYPE, null, "RUNNING"); + static final Object COMPLETED = field(TYPE, null, "COMPLETED"); + } + static HttpInboundStoreAccess open(Path parent, String directory) throws IOException { return new HttpInboundStoreAccess((Object) call(TYPE, "open", parent, directory)); } + static HttpInboundStoreAccess inspect(Path parent, String directory) throws IOException { return new HttpInboundStoreAccess((Object) call(TYPE, "inspect", parent, directory)); } + static HttpInboundStoreAccess inspect(Path directory) throws IOException { return new HttpInboundStoreAccess((Object) call(TYPE, "inspect", directory)); } + static Set discover(Path parent) throws IOException { return call(TYPE, "discover", parent); } + Object state(String id) { return call(this, "state", id); } + void reserve(String id) throws IOException { call(this, "reserve", id); } + void markRunning(String id) throws IOException { call(this, "markRunning", id); } + void markCompleted(String id) throws IOException { call(this, "markCompleted", id); } + void seal() { call(this, "seal"); } + void sealAndDeleteIfEmpty() throws IOException { call(this, "sealAndDeleteIfEmpty"); } +} diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpOutgoingOwnershipTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpOutgoingOwnershipTest.java similarity index 74% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpOutgoingOwnershipTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpOutgoingOwnershipTest.java index 1fe22a83..98b4461e 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpOutgoingOwnershipTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpOutgoingOwnershipTest.java @@ -1,4 +1,8 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpEnrollmentAuthority; +import com.bencodez.simpleapi.servercomm.http.HttpProxyTransportServer; +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -21,8 +25,8 @@ void senderWaitingForBackendMonitorCannotReopenJournalAfterClose() throws Except Path root = directory.resolve("outgoing"); HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("proxy"), "localhost"); HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("authority")); - HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), - identity, authority, root, ignored -> { }); + HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), identity, + authority, root, ignored -> { }); var field = HttpProxyTransportServer.class.getDeclaredField("backends"); field.setAccessible(true); java.util.concurrent.atomic.AtomicBoolean sent = new java.util.concurrent.atomic.AtomicBoolean(true); @@ -38,9 +42,9 @@ void senderWaitingForBackendMonitorCannotReopenJournalAfterClose() throws Except sender.join(3000); assertFalse(sender.isAlive()); assertFalse(sent.get()); - try (HttpProxyTransportServer successor = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), - identity, authority, root, ignored -> { })) { - org.junit.jupiter.api.Assertions.assertNotNull(successor.backendStateForTest("lobby-1")); + try (HttpProxyTransportServer successor = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), identity, + authority, root, ignored -> { })) { + org.junit.jupiter.api.Assertions.assertNotNull(HttpTestAccess.backendStateForTest(successor, "lobby-1")); } } finally { server.close(); @@ -51,21 +55,21 @@ void senderWaitingForBackendMonitorCannotReopenJournalAfterClose() throws Except @Test void outgoingQueueAllowsOneOwnerAndFailsClosedAfterClose() throws Exception { Path root = directory.resolve("outgoing"); - HttpProxyTransportServer.DurableOutgoingQueue owner = new HttpProxyTransportServer.DurableOutgoingQueue(root, + HttpTestAccess.DurableOutgoingQueue owner = new HttpTestAccess.DurableOutgoingQueue(root, com.bencodez.simpleapi.file.DurableFiles::forceDirectory); - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState("lobby-1", owner, + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState("lobby-1", owner, (server, id) -> { }); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(UUID.randomUUID().toString(), + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(UUID.randomUUID().toString(), JsonEnvelope.builder("owner").build()); assertTrue(state.enqueue(delivery)); - assertThrows(IOException.class, () -> new HttpProxyTransportServer.DurableOutgoingQueue(root, + assertThrows(IOException.class, () -> new HttpTestAccess.DurableOutgoingQueue(root, com.bencodez.simpleapi.file.DurableFiles::forceDirectory)); owner.close(); assertThrows(IOException.class, owner::load); - assertFalse(state.enqueue(new HttpTransportProtocol.Delivery(UUID.randomUUID().toString(), + assertFalse(state.enqueue(new HttpProtocolAccess.Delivery(UUID.randomUUID().toString(), JsonEnvelope.builder("closed").build()))); assertThrows(IOException.class, () -> state.acknowledge(List.of(delivery.id()))); - HttpProxyTransportServer.DurableOutgoingQueue successor = new HttpProxyTransportServer.DurableOutgoingQueue(root, + HttpTestAccess.DurableOutgoingQueue successor = new HttpTestAccess.DurableOutgoingQueue(root, com.bencodez.simpleapi.file.DurableFiles::forceDirectory); successor.close(); } @@ -79,7 +83,7 @@ void proxyClaimsOutgoingOwnershipBeforeAnyBackendStateExists() throws Exception authority, root, ignored -> { }); try { assertThrows(IOException.class, () -> new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), identity, - authority, root, ignored -> { }), + authority, root, ignored -> { }), "even an idle proxy must own the durable outgoing queue exclusively"); } finally { owner.close(); } try (HttpProxyTransportServer successor = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), identity, diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpOutgoingQueueCapacityTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpOutgoingQueueCapacityTest.java similarity index 75% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpOutgoingQueueCapacityTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpOutgoingQueueCapacityTest.java index 422c3e4d..955670cd 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpOutgoingQueueCapacityTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpOutgoingQueueCapacityTest.java @@ -1,4 +1,6 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpProxyTransportServer; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -21,36 +23,36 @@ class HttpOutgoingQueueCapacityTest { void quarantineDirectoriesCountTowardCapacityWithoutBlockingSameServerRetry() throws Exception { Path queueRoot = directory.resolve("outgoing"); Files.createDirectory(queueRoot); - HttpTransportProtocol.Delivery existingDelivery = null; + HttpProtocolAccess.Delivery existingDelivery = null; for (int index = 0; index < 128; index++) { String serverId = "server-" + index; String deliveryId = UUID.nameUUIDFromBytes(serverId.getBytes(StandardCharsets.UTF_8)).toString(); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(deliveryId, + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(deliveryId, JsonEnvelope.builder("quarantined-" + index).build()); Path serverDirectory = Files.createDirectory(queueRoot.resolve(serverId)); Files.write(serverDirectory.resolve(".pending-" + deliveryId + ".json"), - HttpTransportProtocol.storedDelivery(delivery)); + HttpProtocolAccess.storedDelivery(delivery)); if (index == 0) existingDelivery = delivery; } - HttpProxyTransportServer.DurableOutgoingQueue queue = new HttpProxyTransportServer.DurableOutgoingQueue( + HttpTestAccess.DurableOutgoingQueue queue = new HttpTestAccess.DurableOutgoingQueue( queueRoot, com.bencodez.simpleapi.file.DurableFiles::forceDirectory); queue.load(); String newServerId = "server-128"; - HttpTransportProtocol.Delivery newDelivery = new HttpTransportProtocol.Delivery(UUID.randomUUID().toString(), + HttpProtocolAccess.Delivery newDelivery = new HttpProtocolAccess.Delivery(UUID.randomUUID().toString(), JsonEnvelope.builder("new-server").build()); - HttpProxyTransportServer.BackendState newServer = new HttpProxyTransportServer.BackendState( + HttpTestAccess.BackendState newServer = new HttpTestAccess.BackendState( newServerId, queue, (server, id) -> { }); assertFalse(newServer.enqueue(newDelivery), "a new backend must be rejected at the durable directory cap"); assertFalse(Files.exists(queueRoot.resolve(newServerId)), "rejected enqueue must not publish a backend directory"); - HttpProxyTransportServer.BackendState existingServer = new HttpProxyTransportServer.BackendState( + HttpTestAccess.BackendState existingServer = new HttpTestAccess.BackendState( "server-0", queue, (server, id) -> { }); assertTrue(existingServer.enqueue(existingDelivery), "same-server retry must recover its quarantine at capacity"); queue.close(); - HttpProxyTransportServer.DurableOutgoingQueue restarted = new HttpProxyTransportServer.DurableOutgoingQueue( + HttpTestAccess.DurableOutgoingQueue restarted = new HttpTestAccess.DurableOutgoingQueue( queueRoot, com.bencodez.simpleapi.file.DurableFiles::forceDirectory); restarted.load(); @@ -65,7 +67,7 @@ void quarantineDirectoriesCountTowardCapacityWithoutBlockingSameServerRetry() th for (Path message : messages.toList()) { if (!message.getFileName().toString().endsWith(".json")) continue; durableFiles++; - assertTrue(deliveryIds.add(HttpTransportProtocol.parseStoredDelivery(Files.readAllBytes(message)).id()), + assertTrue(deliveryIds.add(HttpProtocolAccess.parseStoredDelivery(Files.readAllBytes(message)).id()), "restart must not duplicate a durable delivery"); } } diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpPendingEnrollmentExpiryTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpPendingEnrollmentExpiryTest.java similarity index 84% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpPendingEnrollmentExpiryTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpPendingEnrollmentExpiryTest.java index 9d6cc1ca..0ce998ee 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpPendingEnrollmentExpiryTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpPendingEnrollmentExpiryTest.java @@ -1,4 +1,8 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpConnectionCode; +import com.bencodez.simpleapi.servercomm.http.HttpEnrollmentAuthority; +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -26,7 +30,7 @@ void expiredPendingCertificateCannotActivateButActiveBindingRemainsValid() throw @Override public Instant instant() { return now.get(); } }; HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("identity"), "localhost"); - HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, clock); + HttpEnrollmentAuthority authority = HttpTestAccess.authority(identity, clock); URI endpoint = URI.create("https://localhost:8443/"); HttpConnectionCode pendingCode = authority.createConnectionCode("pending", endpoint, Duration.ofSeconds(1)); var pending = authority.enroll("pending", pendingCode.enrollmentToken()); diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpProtocolAccess.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpProtocolAccess.java new file mode 100644 index 00000000..a28abb94 --- /dev/null +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpProtocolAccess.java @@ -0,0 +1,55 @@ +package com.bencodez.simpleapi.tests.servercomm.http; + +import static com.bencodez.simpleapi.tests.support.TestReflection.*; + +import com.bencodez.simpleapi.tests.support.TestReflection; +import com.bencodez.simpleapi.servercomm.codec.JsonEnvelope; +import java.util.Collection; +import java.util.List; + +/** Reflective views of the real package-private protocol and its record values. */ +final class HttpProtocolAccess { + private static final Class TYPE = type("com.bencodez.simpleapi.servercomm.http.HttpTransportProtocol"); + static final int MAX_BODY_BYTES = (Integer) field(TYPE, null, "MAX_BODY_BYTES"); + static final int MAX_ENVELOPE_BYTES = (Integer) field(TYPE, null, "MAX_ENVELOPE_BYTES"); + static final int MAX_QUEUE = (Integer) field(TYPE, null, "MAX_QUEUE"); + static final long MAX_CLOCK_SKEW_MILLIS = (Long) field(TYPE, null, "MAX_CLOCK_SKEW_MILLIS"); + static byte[] request(String server, String session, long sequence, Collection acks, Collection confirmations, Collection messages) { return call(TYPE, "request", server, session, sequence, acks, confirmations, messages); } + static List fittingMessages(String server, String session, long sequence, Collection acks, Collection confirmations, Collection messages) { return deliveries(call(TYPE, "fittingMessages", server, session, sequence, acks, confirmations, messages)); } + static byte[] storedDelivery(Delivery delivery) { return call(TYPE, "storedDelivery", delivery); } + static Delivery parseStoredDelivery(byte[] body) { return new Delivery(call(TYPE, "parseStoredDelivery", body)); } + static Packet parsePacket(byte[] body) { return new Packet(call(TYPE, "parsePacket", body)); } + static Enrollment parseEnrollment(byte[] body) { return new Enrollment(call(TYPE, "parseEnrollment", body)); } + static List deliveries(Collection values) { return values.stream().map(Delivery::new).toList(); } + static class View implements TestReflection.Handle { + private final Object target; + View(Object target) { this.target = java.util.Objects.requireNonNull(target, "Missing production value"); } + @Override public Object target() { return target; } + @Override public boolean equals(Object other) { return target.equals(unwrap(other)); } + @Override public int hashCode() { return target.hashCode(); } + @Override public String toString() { return target.toString(); } + } + static final class Delivery extends View { + static final Class TYPE = type("com.bencodez.simpleapi.servercomm.http.HttpTransportProtocol$Delivery"); + Delivery(Object target) { super(target); } + Delivery(String id, JsonEnvelope envelope) { super(construct(TYPE, id, envelope)); } + String id() { return call(this, "id"); } + JsonEnvelope envelope() { return call(this, "envelope"); } + } + static final class Packet extends View { + static final Class TYPE = type("com.bencodez.simpleapi.servercomm.http.HttpTransportProtocol$Packet"); + Packet(Object target) { super(target); } + String server() { return call(this, "server"); } + String session() { return call(this, "session"); } + long sequence() { return call(this, "sequence"); } + List acks() { return call(this, "acks"); } + List ackConfirmations() { return call(this, "ackConfirmations"); } + List messages() { return deliveries(call(this, "messages")); } + } + static final class Enrollment extends View { + static final Class TYPE = type("com.bencodez.simpleapi.servercomm.http.HttpTransportProtocol$Enrollment"); + Enrollment(Object target) { super(target); } + String server() { return call(this, "server"); } + String token() { return call(this, "token"); } + } +} diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpProxyLifecycleRecoveryTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpProxyLifecycleRecoveryTest.java similarity index 78% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpProxyLifecycleRecoveryTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpProxyLifecycleRecoveryTest.java index 09cde18a..59431eaf 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpProxyLifecycleRecoveryTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpProxyLifecycleRecoveryTest.java @@ -1,4 +1,11 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpBackendTransportConnector; +import com.bencodez.simpleapi.servercomm.http.HttpClientCredentialStore; +import com.bencodez.simpleapi.servercomm.http.HttpConnectionCode; +import com.bencodez.simpleapi.servercomm.http.HttpEnrollmentAuthority; +import com.bencodez.simpleapi.servercomm.http.HttpProxyTransportServer; +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -34,9 +41,9 @@ void closeDefersOutgoingSealUntilAcknowledgeCallbackReturns() throws Exception { HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("delayed-ack-authority")); HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), identity, authority, outgoing, ignored -> { }); - HttpProxyTransportServer.BackendState state = server.backendStateForTest("lobby-1"); + HttpTestAccess.BackendState state = HttpTestAccess.backendStateForTest(server, "lobby-1"); String id = UUID.randomUUID().toString(); - assertTrue(state.enqueue(new HttpTransportProtocol.Delivery(id, JsonEnvelope.builder("delayed-ack").build()))); + assertTrue(state.enqueue(new HttpProtocolAccess.Delivery(id, JsonEnvelope.builder("delayed-ack").build()))); CountDownLatch started = new CountDownLatch(1), release = new CountDownLatch(1), finished = new CountDownLatch(1); AtomicReference failure = new AtomicReference<>(); ThreadPoolExecutor listener = listenerExecutor(server); @@ -94,7 +101,7 @@ void callbackOwnedCloseCompletesItsInboundJournalBeforeItIsSealed() throws Excep Duration.ofMinutes(5)); HttpClientCredentialStore.ClientCredential credential = HttpBackendTransportConnector.enroll(code, "lobby-1", directory.resolve("client")); - try (HttpBackendTransportConnector connector = new HttpBackendTransportConnector(code, "lobby-1", credential, + try (HttpBackendTransportConnector connector = HttpTestAccess.connector(code, "lobby-1", credential, ignored -> { })) { connector.start(); assertTrue(connector.awaitFirstResponse(System.nanoTime() + TimeUnit.SECONDS.toNanos(8))); @@ -105,8 +112,8 @@ void callbackOwnedCloseCompletesItsInboundJournalBeforeItIsSealed() throws Excep } server.close(); assertNotNull(deliveryId.get()); - assertEquals(HttpInboundDeliveryStore.State.COMPLETED, - HttpInboundDeliveryStore.inspect(directory.resolve("outgoing-incoming"), "lobby-1").state(deliveryId.get()), + assertEquals(HttpInboundStoreAccess.State.COMPLETED, + HttpInboundStoreAccess.inspect(directory.resolve("outgoing-incoming"), "lobby-1").state(deliveryId.get()), "the callback completion must be durable before close seals the journal"); } finally { server.close(); @@ -117,14 +124,14 @@ void callbackOwnedCloseCompletesItsInboundJournalBeforeItIsSealed() throws Excep void failedNewBackendSetupCleansItsEmptyDirectoryBeforeRetry() throws Exception { Path root = directory.resolve("outgoing"); AtomicBoolean failFirstBackendPublication = new AtomicBoolean(true); - HttpProxyTransportServer.DurableOutgoingQueue queue = new HttpProxyTransportServer.DurableOutgoingQueue(root, + HttpTestAccess.DurableOutgoingQueue queue = new HttpTestAccess.DurableOutgoingQueue(root, forced -> { if (forced.equals(root) && failFirstBackendPublication.getAndSet(false)) throw new IOException("injected backend directory publication failure"); }); - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState("lobby-1", queue, + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState("lobby-1", queue, (server, id) -> { }); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(UUID.randomUUID().toString(), + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(UUID.randomUUID().toString(), JsonEnvelope.builder("retry-directory").build()); assertFalse(state.enqueue(delivery)); @@ -137,17 +144,17 @@ void failedNewBackendSetupCleansItsEmptyDirectoryBeforeRetry() throws Exception @Test void restartPrunesAccumulatedEmptyBackendDirectoriesBeforeApplyingTheCap() throws Exception { Path root = directory.resolve("outgoing"); - HttpProxyTransportServer.DurableOutgoingQueue initial = new HttpProxyTransportServer.DurableOutgoingQueue(root, + HttpTestAccess.DurableOutgoingQueue initial = new HttpTestAccess.DurableOutgoingQueue(root, ignored -> { }); initial.close(); for (int index = 0; index < 128; index++) Files.createDirectory(root.resolve("server-" + index)); - HttpProxyTransportServer.DurableOutgoingQueue restarted = new HttpProxyTransportServer.DurableOutgoingQueue(root, + HttpTestAccess.DurableOutgoingQueue restarted = new HttpTestAccess.DurableOutgoingQueue(root, ignored -> { }); assertTrue(restarted.load().isEmpty()); - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState("replacement", restarted, + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState("replacement", restarted, (server, id) -> { }); - assertTrue(state.enqueue(new HttpTransportProtocol.Delivery(UUID.randomUUID().toString(), + assertTrue(state.enqueue(new HttpProtocolAccess.Delivery(UUID.randomUUID().toString(), JsonEnvelope.builder("recovered-capacity").build()))); assertTrue(Files.isDirectory(root.resolve("replacement"))); restarted.close(); @@ -157,11 +164,11 @@ void restartPrunesAccumulatedEmptyBackendDirectoriesBeforeApplyingTheCap() throw void capPruningPreservesAnEmptyDirectoryUntilItsUnconfirmedAckCanRetry() throws Exception { Path root = directory.resolve("outgoing"); Path lobbyDirectory = root.resolve("lobby-1"); - HttpProxyTransportServer.DurableOutgoingQueue queue = new HttpProxyTransportServer.DurableOutgoingQueue(root, + HttpTestAccess.DurableOutgoingQueue queue = new HttpTestAccess.DurableOutgoingQueue(root, ignored -> { }); - HttpProxyTransportServer.BackendState lobby = new HttpProxyTransportServer.BackendState("lobby-1", queue, + HttpTestAccess.BackendState lobby = new HttpTestAccess.BackendState("lobby-1", queue, (server, id) -> { }); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(UUID.randomUUID().toString(), + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(UUID.randomUUID().toString(), JsonEnvelope.builder("ack-retry").build()); assertTrue(lobby.enqueue(delivery)); @@ -175,9 +182,9 @@ void capPruningPreservesAnEmptyDirectoryUntilItsUnconfirmedAckCanRetry() throws assertThrows(IOException.class, () -> lobby.acknowledge(List.of(delivery.id()))); assertTrue(Files.isDirectory(lobbyDirectory), "the unlink has happened but its directory fsync is unresolved"); - HttpProxyTransportServer.BackendState replacement = new HttpProxyTransportServer.BackendState("replacement", + HttpTestAccess.BackendState replacement = new HttpTestAccess.BackendState("replacement", queue, (server, id) -> { }); - assertTrue(replacement.enqueue(new HttpTransportProtocol.Delivery(UUID.randomUUID().toString(), + assertTrue(replacement.enqueue(new HttpProtocolAccess.Delivery(UUID.randomUUID().toString(), JsonEnvelope.builder("trigger-cap-count").build()))); assertTrue(Files.isDirectory(lobbyDirectory), "capacity pruning must retain the directory indexed by the failed acknowledgement"); diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpRenewalRateLimitTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpRenewalRateLimitTest.java similarity index 84% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpRenewalRateLimitTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpRenewalRateLimitTest.java index 039d7103..8a3ed512 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpRenewalRateLimitTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpRenewalRateLimitTest.java @@ -1,4 +1,7 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpEnrollmentAuthority; +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -27,16 +30,16 @@ void repeatedRenewalIsLimitedPerBackendBeforeCertificateIssuance() throws Except @Override public Instant instant() { return now.get(); } }; HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("identity"), "localhost"); - HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, clock); + HttpEnrollmentAuthority authority = HttpTestAccess.authority(identity, clock); URI endpoint = URI.create("https://localhost:8443/"); var code = authority.createConnectionCode("lobby-1", endpoint, Duration.ofMinutes(5)); var original = authority.enroll("lobby-1", code.enrollmentToken()); assertTrue(authority.authenticate("lobby-1", original.certificate())); var replacement = authority.renew("lobby-1", original.certificate()); - for (int index = 0; index < 10; index++) assertThrows(HttpEnrollmentAuthority.RenewalRateLimitException.class, + for (int index = 0; index < 10; index++) assertThrows(HttpTestAccess.RENEWAL_RATE_LIMIT, () -> authority.renew("lobby-1", original.certificate())); assertTrue(authority.authenticate("lobby-1", replacement.certificate())); - assertThrows(HttpEnrollmentAuthority.RenewalRateLimitException.class, + assertThrows(HttpTestAccess.RENEWAL_RATE_LIMIT, () -> authority.renew("lobby-1", replacement.certificate())); var otherCode = authority.createConnectionCode("lobby-2", endpoint, Duration.ofMinutes(5)); var other = authority.enroll("lobby-2", otherCode.enrollmentToken()); @@ -57,15 +60,15 @@ void renewalWindowIsSharedAcrossAuthorities() throws Exception { Path stateDirectory = directory.resolve("shared-authority"); java.nio.file.Files.createDirectory(stateDirectory); Path stateFile = stateDirectory.resolve("http-transport-clients.properties"); - HttpEnrollmentAuthority first = new HttpEnrollmentAuthority(identity, clock, stateFile); - HttpEnrollmentAuthority second = new HttpEnrollmentAuthority(identity, clock, stateFile); + HttpEnrollmentAuthority first = HttpTestAccess.authority(identity, clock, stateFile); + HttpEnrollmentAuthority second = HttpTestAccess.authority(identity, clock, stateFile); URI endpoint = URI.create("https://localhost:8443/"); var code = first.createConnectionCode("lobby-1", endpoint, Duration.ofMinutes(5)); var original = first.enroll("lobby-1", code.enrollmentToken()); assertTrue(first.authenticate("lobby-1", original.certificate())); first.renew("lobby-1", original.certificate()); - assertThrows(HttpEnrollmentAuthority.RenewalRateLimitException.class, + assertThrows(HttpTestAccess.RENEWAL_RATE_LIMIT, () -> second.renew("lobby-1", original.certificate())); now.set(now.get().plusSeconds(60)); assertNotNull(second.renew("lobby-1", original.certificate())); @@ -79,7 +82,7 @@ void revocationRemovesTheDurableRenewalWindow() throws Exception { Path stateDirectory = directory.resolve("revoked-authority"); java.nio.file.Files.createDirectory(stateDirectory); Path stateFile = stateDirectory.resolve("http-transport-clients.properties"); - HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, clock, stateFile); + HttpEnrollmentAuthority authority = HttpTestAccess.authority(identity, clock, stateFile); URI endpoint = URI.create("https://localhost:8443/"); var firstCode = authority.createConnectionCode("lobby-1", endpoint, Duration.ofMinutes(5)); var firstCertificate = authority.enroll("lobby-1", firstCode.enrollmentToken()); @@ -90,7 +93,7 @@ void revocationRemovesTheDurableRenewalWindow() throws Exception { var secondCode = authority.createConnectionCode("lobby-1", endpoint, Duration.ofMinutes(5)); var secondCertificate = authority.enroll("lobby-1", secondCode.enrollmentToken()); assertTrue(authority.authenticate("lobby-1", secondCertificate.certificate())); - assertNotNull(new HttpEnrollmentAuthority(identity, clock, stateFile) + assertNotNull(HttpTestAccess.authority(identity, clock, stateFile) .renew("lobby-1", secondCertificate.certificate())); } } diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpRevocationRetryIsolationTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpRevocationRetryIsolationTest.java similarity index 92% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpRevocationRetryIsolationTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpRevocationRetryIsolationTest.java index ad4b7836..63503928 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpRevocationRetryIsolationTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpRevocationRetryIsolationTest.java @@ -1,4 +1,8 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpConnectionCode; +import com.bencodez.simpleapi.servercomm.http.HttpEnrollmentAuthority; +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpSecretsAccess.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpSecretsAccess.java new file mode 100644 index 00000000..3445c53d --- /dev/null +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpSecretsAccess.java @@ -0,0 +1,13 @@ +package com.bencodez.simpleapi.tests.servercomm.http; + +import static com.bencodez.simpleapi.tests.support.TestReflection.*; + +import java.security.cert.X509Certificate; + +/** Calls production secret utilities rather than reproducing cryptographic logic in tests. */ +final class HttpSecretsAccess { + private static final Class TYPE = type("com.bencodez.simpleapi.servercomm.http.HttpTransportSecrets"); + static String randomToken() { return call(TYPE, "randomToken"); } + static String certificatePin(X509Certificate certificate) { return call(TYPE, "certificatePin", certificate); } + static String sha256Hex(byte[] value) { return call(TYPE, "sha256Hex", value); } +} diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpTestAccess.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpTestAccess.java new file mode 100644 index 00000000..f280934c --- /dev/null +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpTestAccess.java @@ -0,0 +1,135 @@ +package com.bencodez.simpleapi.tests.servercomm.http; + +import static com.bencodez.simpleapi.tests.support.TestReflection.*; + +import com.bencodez.simpleapi.servercomm.http.HttpBackendTransportConnector; +import com.bencodez.simpleapi.servercomm.http.HttpClientCredentialStore; +import com.bencodez.simpleapi.servercomm.http.HttpConnectionCode; +import com.bencodez.simpleapi.servercomm.http.HttpEnrollmentAuthority; +import com.bencodez.simpleapi.servercomm.http.HttpEnvelopeWireCodec; +import com.bencodez.simpleapi.servercomm.http.HttpProxyTransportServer; +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; +import com.bencodez.simpleapi.servercomm.codec.JsonEnvelope; +import com.sun.net.httpserver.Headers; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.nio.file.Path; +import java.security.cert.X509Certificate; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.function.Consumer; +import java.util.function.LongSupplier; +import javax.net.ssl.TrustManager; + +/** Narrow test-only entry points for the original HTTP implementation internals. */ +final class HttpTestAccess { + @SuppressWarnings("unchecked") + static final Class RENEWAL_RATE_LIMIT = (Class) type("com.bencodez.simpleapi.servercomm.http.HttpEnrollmentAuthority$RenewalRateLimitException"); + static final Duration LONG_POLL = (Duration) field(HttpProxyTransportServer.class, null, "LONG_POLL"); + static final int CALLBACK_QUEUE_CAPACITY = (Integer) field(HttpBackendTransportConnector.class, null, "CALLBACK_QUEUE_CAPACITY"); + static HttpProxyTransportServer server(InetSocketAddress bind, HttpTlsIdentity identity, HttpEnrollmentAuthority authority, Consumer callback) throws Exception { return construct(HttpProxyTransportServer.class, bind, identity, authority, callback); } + static HttpProxyTransportServer server(InetSocketAddress bind, HttpTlsIdentity identity, HttpEnrollmentAuthority authority, Path directory, Consumer callback, HttpProxyTransportServer.DeliveryAcknowledgement acknowledged, LongSupplier clock) throws Exception { return construct(HttpProxyTransportServer.class, bind, identity, authority, directory, callback, acknowledged, clock); } + static HttpBackendTransportConnector connector(HttpConnectionCode code, String server, HttpClientCredentialStore.ClientCredential credential, Consumer callback) throws Exception { return construct(HttpBackendTransportConnector.class, code, server, credential, callback); } + static HttpBackendTransportConnector connector(HttpClientCredentialStore.HttpClientProfile profile, HttpClientCredentialStore.ClientCredential credential, Consumer callback) throws Exception { return construct(HttpBackendTransportConnector.class, profile, credential, callback); } + static HttpEnrollmentAuthority authority(HttpTlsIdentity identity, Clock clock) throws Exception { return construct(HttpEnrollmentAuthority.class, identity, clock); } + static HttpEnrollmentAuthority authority(HttpTlsIdentity identity, Clock clock, Path stateFile) throws Exception { return construct(HttpEnrollmentAuthority.class, identity, clock, stateFile); } + static HttpTlsIdentity identity(Path directory, String host, Clock clock) throws Exception { return call(HttpTlsIdentity.class, "loadOrCreate", directory, host, clock); } + static HttpTlsIdentity.IssuedClientCertificate issueClientCertificate(HttpTlsIdentity identity, String server, Instant issuedAt) throws Exception { return call(identity, "issueClientCertificate", server, issuedAt); } + static int fixedBodyErrorStatus(Headers headers, int maximum) { return call(HttpProxyTransportServer.class, "fixedBodyErrorStatus", headers, maximum); } + static boolean shutdown(ExecutorService executor) { return call(HttpProxyTransportServer.class, "shutdown", executor); } + static byte[] readLimited(InputStream body) throws IOException { return call(HttpBackendTransportConnector.class, "readLimited", body); } + static Duration renewalRetryDelay(Duration remaining) { return call(HttpBackendTransportConnector.class, "renewalRetryDelay", remaining); } + static long renewalDeadline(long now, Duration delay) { return call(HttpBackendTransportConnector.class, "renewalDeadline", now, delay); } + static long timeoutNanos(Duration timeout) { return call(HttpBackendTransportConnector.class, "timeoutNanos", timeout); } + static LimitedResponse sendLimited(HttpClient client, HttpRequest request) throws IOException, InterruptedException { return new LimitedResponse(call(HttpBackendTransportConnector.class, "sendLimited", client, request)); } + static boolean needsRenewal(X509Certificate certificate, Clock clock) { return call(HttpTlsIdentity.class, "needsRenewal", certificate, clock); } + static boolean needsCaRenewal(X509Certificate certificate, Clock clock) { return call(HttpTlsIdentity.class, "needsCaRenewal", certificate, clock); } + static TrustManager[] trustManagers(X509Certificate certificate) throws Exception { return call(HttpTlsIdentity.class, "trustManagers", certificate); } + static boolean isIpLiteral(String value) { return call(type("com.bencodez.simpleapi.servercomm.http.JdkX509CertificateGenerator"), "isIpLiteral", value); } + static String encodeLegacy(HttpConnectionCode code) { return call(code, "encodeLegacy"); } + static StagedCredential stageReplacement(Path directory, HttpTlsIdentity.IssuedClientCertificate issued) throws Exception { return new StagedCredential(call(HttpClientCredentialStore.class, "stageReplacement", directory, issued)); } + static void activateReplacement(Path directory, StagedCredential staged) throws IOException { call(HttpClientCredentialStore.class, "activateReplacement", directory, staged); } + static BackendState backendStateForTest(HttpProxyTransportServer server, String id) throws IOException { return new BackendState((Object) call(server, "backendStateForTest", id)); } + static int backendCountForTest(HttpProxyTransportServer server) { return call(server, "backendCountForTest"); } + static boolean pollerAlive(HttpBackendTransportConnector connector) { return call(connector, "pollerAlive"); } + static List accept(HttpBackendTransportConnector connector, List deliveries) { return HttpProtocolAccess.deliveries(call(connector, "accept", deliveries)); } + static void dispatch(HttpBackendTransportConnector connector, HttpProtocolAccess.Delivery delivery) { call(connector, "dispatch", delivery); } + static void completeIncoming(HttpBackendTransportConnector connector, String id, boolean success) { call(connector, "completeIncoming", id, success); } + static int queuedOutgoing(HttpBackendTransportConnector connector) { return call(connector, "queuedOutgoing"); } + static List drainAcknowledgements(HttpBackendTransportConnector connector) { return call(connector, "drainAcknowledgements"); } + @FunctionalInterface interface EnrollmentOperation { T run() throws Exception; } + static T withEnrollmentLock(Path directory, EnrollmentOperation operation) throws Exception { + Class operationType = type("com.bencodez.simpleapi.servercomm.http.HttpClientCredentialStore$EnrollmentOperation"); + Object callback = java.lang.reflect.Proxy.newProxyInstance(operationType.getClassLoader(), new Class[] { operationType }, + (proxy, method, arguments) -> operation.run()); + return call(HttpClientCredentialStore.class, "withEnrollmentLock", directory, callback); + } + static final class StagedCredential extends HttpProtocolAccess.View { + static final Class TYPE = type("com.bencodez.simpleapi.servercomm.http.HttpClientCredentialStore$StagedCredential"); + StagedCredential(Object target) { super(target); } + String name() { return call(this, "name"); } + HttpClientCredentialStore.ClientCredential credential() { return call(this, "credential"); } + HttpClientCredentialStore.HttpClientProfile profile() { return call(this, "profile"); } + } + static final class LimitedResponse extends HttpProtocolAccess.View { + static final Class TYPE = type("com.bencodez.simpleapi.servercomm.http.HttpBackendTransportConnector$LimitedResponse"); + LimitedResponse(Object target) { super(target); } + int statusCode() { return call(this, "statusCode"); } + byte[] body() { return call(this, "body"); } + } + static final class Response extends HttpProtocolAccess.View { + static final Class TYPE = type("com.bencodez.simpleapi.servercomm.http.HttpProxyTransportServer$Response"); + Response(Object target) { super(target); } + Collection acks() { return call(this, "acks"); } + Collection messages() { return HttpProtocolAccess.deliveries(call(this, "messages")); } + } + static final class BackendState extends HttpProtocolAccess.View { + static final Class TYPE = type("com.bencodez.simpleapi.servercomm.http.HttpProxyTransportServer$BackendState"); + BackendState(Object target) { super(target); } + BackendState() { super(construct(TYPE)); } + BackendState(LongSupplier clock) { super(construct(TYPE, clock)); } + BackendState(String id, DurableOutgoingQueue queue, HttpProxyTransportServer.DeliveryAcknowledgement acknowledged) { super(construct(TYPE, id, queue, acknowledged)); } + BackendState(String id, DurableOutgoingQueue queue, HttpInboundStoreAccess inbound, HttpProxyTransportServer.DeliveryAcknowledgement acknowledged) { super(construct(TYPE, id, queue, inbound, acknowledged)); } + boolean beginPollForTest() { return call(this, "beginPollForTest"); } + void endPollForTest() { call(this, "endPollForTest"); } + boolean acceptSession(String session, long sequence) { return call(this, "acceptSession", session, sequence); } + List acceptIncoming(List deliveries) { return HttpProtocolAccess.deliveries(call(this, "acceptIncoming", deliveries)); } + boolean enqueue(HttpProtocolAccess.Delivery delivery) { return call(this, "enqueue", delivery); } + void acknowledge(Collection ids) throws IOException { call(this, "acknowledge", ids); } + void beginIncoming(String id) throws IOException { call(this, "beginIncoming", id); } + void completeIncomingDurably(String id) throws IOException { call(this, "completeIncomingDurably", id); } + void completeIncoming(String id, boolean success) { call(this, "completeIncoming", id, success); } + void confirmIncoming(Collection ids) throws IOException { call(this, "confirmIncoming", ids); } + Response await(String server, String session, long sequence) { return new Response(call(this, "await", server, session, sequence)); } + Response await(String server, String session, long sequence, Collection confirmations) { return new Response(call(this, "await", server, session, sequence, confirmations)); } + Response await(String server, String session, long sequence, Collection confirmations, HttpEnvelopeWireCodec codec) { return new Response(call(this, "await", server, session, sequence, confirmations, codec)); } + } + @FunctionalInterface interface DirectoryForcer { void force(Path directory) throws IOException; } + static final class DurableOutgoingQueue extends HttpProtocolAccess.View implements AutoCloseable { + static final Class TYPE = type("com.bencodez.simpleapi.servercomm.http.HttpProxyTransportServer$DurableOutgoingQueue"); + DurableOutgoingQueue(Path directory, DirectoryForcer forcer) throws IOException { super(create(directory, forcer)); } + private static Object create(Path directory, DirectoryForcer forcer) { + Class callbackType = type("com.bencodez.simpleapi.servercomm.http.HttpProxyTransportServer$DurableOutgoingQueue$DirectoryForcer"); + Object callback = java.lang.reflect.Proxy.newProxyInstance(callbackType.getClassLoader(), new Class[] { callbackType }, + (proxy, method, arguments) -> { forcer.force((Path) arguments[0]); return null; }); + return construct(TYPE, directory, callback); + } + boolean hasPendingDeliveries() { return call(this, "hasPendingDeliveries"); } + Map> load() throws IOException { + Map> loaded = call(this, "load"); + Map> views = new LinkedHashMap<>(); + loaded.forEach((server, deliveries) -> views.put(server, HttpProtocolAccess.deliveries(deliveries))); + return views; + } + @Override public void close() { call(this, "close"); } + } +} diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpTlsIdentityOwnershipTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpTlsIdentityOwnershipTest.java similarity index 93% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpTlsIdentityOwnershipTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpTlsIdentityOwnershipTest.java index bdd39f97..e85624be 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpTlsIdentityOwnershipTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpTlsIdentityOwnershipTest.java @@ -1,4 +1,6 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -96,9 +98,9 @@ void staleInstanceAdoptsAnotherInstancesRenewedServerCertificate() throws Except Instant now = Instant.now(); Path identityDirectory = directory.resolve("stale-renewal"); Clock creationClock = Clock.fixed(now.minus(Duration.ofDays(340)), ZoneOffset.UTC); - HttpTlsIdentity first = HttpTlsIdentity.loadOrCreate(identityDirectory, "localhost", creationClock); - HttpTlsIdentity second = HttpTlsIdentity.loadOrCreate(identityDirectory, "localhost", creationClock); - String originalPin = HttpTransportSecrets.certificatePin(first.serverCertificate()); + HttpTlsIdentity first = HttpTestAccess.identity(identityDirectory, "localhost", creationClock); + HttpTlsIdentity second = HttpTestAccess.identity(identityDirectory, "localhost", creationClock); + String originalPin = HttpSecretsAccess.certificatePin(first.serverCertificate()); String renewedByFirst = first.serverCertificatePin(); assertNotEquals(originalPin, renewedByFirst); diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpTransportRuntimeTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpTransportRuntimeTest.java similarity index 80% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpTransportRuntimeTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpTransportRuntimeTest.java index 2cbf2177..abc62ffb 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpTransportRuntimeTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpTransportRuntimeTest.java @@ -1,4 +1,12 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpBackendTransportConnector; +import com.bencodez.simpleapi.servercomm.http.HttpClientCredentialStore; +import com.bencodez.simpleapi.servercomm.http.HttpConnectionCode; +import com.bencodez.simpleapi.servercomm.http.HttpEnrollmentAuthority; +import com.bencodez.simpleapi.servercomm.http.HttpPinnedTls; +import com.bencodez.simpleapi.servercomm.http.HttpProxyTransportServer; +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -51,7 +59,7 @@ void pendingDeliveryStateTracksDurableQueueUntilAcknowledgement() throws Excepti assertTrue(server.send("lobby-1", deliveryId, JsonEnvelope.builder("ordinary").build())); assertTrue(server.hasPendingDeliveries()); - server.backendStateForTest("lobby-1").acknowledge(java.util.List.of(deliveryId)); + HttpTestAccess.backendStateForTest(server, "lobby-1").acknowledge(java.util.List.of(deliveryId)); assertFalse(server.hasPendingDeliveries()); } @@ -113,8 +121,8 @@ void enrollsThenDeliversBothDirectionsWithAuthenticatedIdentity() throws Excepti assertEquals("lobby-1", received.get().serverId()); assertEquals("lobby-1", received.get().envelope().getFields().get("server")); long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3); - while (connector.queuedOutgoing() != 0 && System.nanoTime() < deadline) Thread.sleep(10); - assertEquals(0, connector.queuedOutgoing(), "proxy ACK must remove the exact outbound delivery ID"); + while (HttpTestAccess.queuedOutgoing(connector) != 0 && System.nanoTime() < deadline) Thread.sleep(10); + assertEquals(0, HttpTestAccess.queuedOutgoing(connector), "proxy ACK must remove the exact outbound delivery ID"); Path proxyInboundFence = directory.resolve("proxy-outgoing-incoming").resolve("lobby-1"); long confirmationDeadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3); while (countRegularFiles(proxyInboundFence) != 0L && System.nanoTime() < confirmationDeadline) Thread.sleep(10); @@ -154,7 +162,7 @@ void responseBodyConsumptionRemainsBoundedByRequestTimeout() throws Exception { HttpRequest request = HttpRequest.newBuilder(URI.create("http://localhost:" + server.getAddress().getPort() + "/stall")).timeout(Duration.ofMillis(250)).GET().build(); assertTimeoutPreemptively(Duration.ofSeconds(2), () -> assertThrows(java.io.IOException.class, - () -> HttpBackendTransportConnector.sendLimited(HttpClient.newHttpClient(), request))); + () -> HttpTestAccess.sendLimited(HttpClient.newHttpClient(), request))); } finally { release.countDown(); server.stop(0); @@ -164,14 +172,14 @@ void responseBodyConsumptionRemainsBoundedByRequestTimeout() throws Exception { @Test void stableProxyDeliveryIdsAreIdempotentAndAcknowledgedBeforeRemoval() throws Exception { AtomicLong acknowledged = new AtomicLong(); - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState("lobby-1", null, + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState("lobby-1", null, (server, deliveryId) -> acknowledged.incrementAndGet()); String deliveryId = java.util.UUID.randomUUID().toString(); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(deliveryId, + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(deliveryId, JsonEnvelope.builder("vote-party").build()); assertTrue(state.enqueue(delivery)); assertTrue(state.enqueue(delivery)); - assertFalse(state.enqueue(new HttpTransportProtocol.Delivery(deliveryId, + assertFalse(state.enqueue(new HttpProtocolAccess.Delivery(deliveryId, JsonEnvelope.builder("different").build()))); state.acknowledge(java.util.List.of(deliveryId)); assertEquals(1L, acknowledged.get()); @@ -182,14 +190,14 @@ void stableProxyDeliveryIdsAreIdempotentAndAcknowledgedBeforeRemoval() throws Ex void outgoingQueueRetriesDirectoryPublicationAfterForceFailure() throws Exception { Path queueRoot = directory.resolve("retry-outgoing-root"); java.util.concurrent.atomic.AtomicBoolean failRootPublication = new java.util.concurrent.atomic.AtomicBoolean(true); - assertThrows(java.io.IOException.class, () -> new HttpProxyTransportServer.DurableOutgoingQueue(queueRoot, + assertThrows(java.io.IOException.class, () -> new HttpTestAccess.DurableOutgoingQueue(queueRoot, ignored -> { if (failRootPublication.getAndSet(false)) throw new java.io.IOException("injected root publication failure"); })); assertTrue(Files.isDirectory(queueRoot), "the failed force occurs after the root name is published"); AtomicLong parentForces = new AtomicLong(); - HttpProxyTransportServer.DurableOutgoingQueue queue = new HttpProxyTransportServer.DurableOutgoingQueue( + HttpTestAccess.DurableOutgoingQueue queue = new HttpTestAccess.DurableOutgoingQueue( queueRoot, forced -> { if (forced.equals(queueRoot.getParent())) parentForces.incrementAndGet(); }); @@ -197,7 +205,7 @@ void outgoingQueueRetriesDirectoryPublicationAfterForceFailure() throws Exceptio AtomicLong serverRootForces = new AtomicLong(); java.util.concurrent.atomic.AtomicBoolean failServerPublication = new java.util.concurrent.atomic.AtomicBoolean(true); - HttpProxyTransportServer.DurableOutgoingQueue serverQueue = new HttpProxyTransportServer.DurableOutgoingQueue( + HttpTestAccess.DurableOutgoingQueue serverQueue = new HttpTestAccess.DurableOutgoingQueue( directory.resolve("retry-outgoing-server"), forced -> { if (forced.getFileName().toString().equals("retry-outgoing-server")) { serverRootForces.incrementAndGet(); @@ -205,9 +213,9 @@ void outgoingQueueRetriesDirectoryPublicationAfterForceFailure() throws Exceptio throw new java.io.IOException("injected backend publication failure"); } }); - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState( + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState( "lobby-1", serverQueue, (server, id) -> { }); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery( + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery( java.util.UUID.randomUUID().toString(), JsonEnvelope.builder("retry-directory-force").build()); assertFalse(state.enqueue(delivery)); assertEquals(2L, serverRootForces.get(), "failed publication and empty-directory cleanup must both force the parent"); @@ -241,20 +249,20 @@ void generatedSendExposesRecoverableIdAfterPublicationFailure() throws Exception retryId = retry.deliveryId(); assertEquals(retryId, java.util.UUID.fromString(retryId).toString()); } - assertTrue(server.backendStateForTest("lobby-1") + assertTrue(HttpTestAccess.backendStateForTest(server, "lobby-1") .await("lobby-1", java.util.UUID.randomUUID().toString(), 0).messages().isEmpty()); } try (HttpProxyTransportServer restarted = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), identity, authority, queueRoot, ignored -> { })) { - assertTrue(restarted.backendStateForTest("lobby-1") + assertTrue(HttpTestAccess.backendStateForTest(restarted, "lobby-1") .await("lobby-1", java.util.UUID.randomUUID().toString(), 0).messages().isEmpty()); assertTrue(restarted.send("lobby-1", retryId, envelope)); assertEquals(1L, countRegularFiles(queueRoot)); - var messages = restarted.backendStateForTest("lobby-1") + var messages = HttpTestAccess.backendStateForTest(restarted, "lobby-1") .await("lobby-1", java.util.UUID.randomUUID().toString(), 0).messages(); assertEquals(1, messages.size()); assertEquals(retryId, messages.iterator().next().id()); - restarted.backendStateForTest("lobby-1").acknowledge(java.util.List.of(retryId)); + HttpTestAccess.backendStateForTest(restarted, "lobby-1").acknowledge(java.util.List.of(retryId)); assertEquals(0L, countRegularFiles(queueRoot)); } } @@ -267,7 +275,7 @@ void outgoingQueueRecoversWhenPromotionQuarantineRenameFails() throws Exception Path serverDirectory = queueRoot.resolve("lobby-1"); Path pending = serverDirectory.resolve(".pending-" + deliveryId + ".json"); AtomicLong serverForces = new AtomicLong(); - HttpProxyTransportServer.DurableOutgoingQueue queue = new HttpProxyTransportServer.DurableOutgoingQueue(queueRoot, + HttpTestAccess.DurableOutgoingQueue queue = new HttpTestAccess.DurableOutgoingQueue(queueRoot, forced -> { if (!forced.equals(serverDirectory)) return; long attempt = serverForces.incrementAndGet(); @@ -277,18 +285,18 @@ void outgoingQueueRecoversWhenPromotionQuarantineRenameFails() throws Exception throw new java.io.IOException("injected promotion publication failure"); } }); - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState("lobby-1", queue, + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState("lobby-1", queue, (server, id) -> { }); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(deliveryId, + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(deliveryId, JsonEnvelope.builder("promotion-rename-failure").build()); assertFalse(state.enqueue(delivery)); assertThrows(IllegalStateException.class, () -> state.enqueue(delivery)); Files.delete(pending); assertTrue(state.enqueue(delivery), "same-ID retry must confirm the observed target instead of duplicating it"); - var quarantinedFiles = HttpProxyTransportServer.DurableOutgoingQueue.class.getDeclaredField("quarantinedFiles"); + var quarantinedFiles = HttpTestAccess.DurableOutgoingQueue.TYPE.getDeclaredField("quarantinedFiles"); quarantinedFiles.setAccessible(true); @SuppressWarnings("unchecked") - var quarantined = (java.util.Map>) quarantinedFiles.get(queue); + var quarantined = (java.util.Map>) quarantinedFiles.get(queue.target()); assertFalse(quarantined.get("lobby-1").containsKey(deliveryId), "target fallback must clear the stale quarantine index"); state.acknowledge(java.util.List.of(deliveryId)); @@ -300,14 +308,14 @@ void outgoingQueueRecoversWhenPromotionQuarantineRenameFails() throws Exception void publishedOutgoingDeliveryRemainsTrackedUntilDurabilityCanBeConfirmed() throws Exception { AtomicLong forceCalls = new AtomicLong(); Path queueRoot = directory.resolve("uncertain-outgoing"); - HttpProxyTransportServer.DurableOutgoingQueue queue = new HttpProxyTransportServer.DurableOutgoingQueue( + HttpTestAccess.DurableOutgoingQueue queue = new HttpTestAccess.DurableOutgoingQueue( queueRoot, ignored -> { if (forceCalls.incrementAndGet() == 3L) throw new java.io.IOException("injected directory force failure"); }); - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState( + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState( "lobby-1", queue, (server, id) -> { }); String deliveryId = java.util.UUID.randomUUID().toString(); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(deliveryId, + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(deliveryId, JsonEnvelope.builder("durable").build()); assertFalse(state.enqueue(delivery), "post-publication failure must not confirm durable acceptance"); @@ -317,11 +325,11 @@ void publishedOutgoingDeliveryRemainsTrackedUntilDurabilityCanBeConfirmed() thro "an uncertain publication must remain hidden until its durability retry succeeds"); assertEquals(1L, countRegularFiles(queueRoot)); queue.close(); - HttpProxyTransportServer.DurableOutgoingQueue restartedQueue = new HttpProxyTransportServer.DurableOutgoingQueue( + HttpTestAccess.DurableOutgoingQueue restartedQueue = new HttpTestAccess.DurableOutgoingQueue( queueRoot, com.bencodez.simpleapi.file.DurableFiles::forceDirectory); assertEquals(java.util.List.of(), restartedQueue.load().get("lobby-1"), "a quarantine must reserve its backend without exposing an operation whose sender observed rejection"); - HttpProxyTransportServer.BackendState restarted = new HttpProxyTransportServer.BackendState( + HttpTestAccess.BackendState restarted = new HttpTestAccess.BackendState( "lobby-1", restartedQueue, (server, id) -> { }); assertTrue(restarted.await("lobby-1", java.util.UUID.randomUUID().toString(), 0).messages().isEmpty()); assertTrue(restarted.enqueue(delivery), "same-ID retry must confirm the quarantined file"); @@ -329,14 +337,14 @@ void publishedOutgoingDeliveryRemainsTrackedUntilDurabilityCanBeConfirmed() thro assertEquals(java.util.List.of(delivery), restarted.await("lobby-1", java.util.UUID.randomUUID().toString(), 0).messages()); restartedQueue.close(); - HttpProxyTransportServer.DurableOutgoingQueue confirmedQueue = new HttpProxyTransportServer.DurableOutgoingQueue( + HttpTestAccess.DurableOutgoingQueue confirmedQueue = new HttpTestAccess.DurableOutgoingQueue( queueRoot, com.bencodez.simpleapi.file.DurableFiles::forceDirectory); - java.util.List confirmed = confirmedQueue.load().get("lobby-1"); + java.util.List confirmed = confirmedQueue.load().get("lobby-1"); assertEquals(1, confirmed.size()); - assertTrue(java.util.Arrays.equals(HttpTransportProtocol.storedDelivery(delivery), - HttpTransportProtocol.storedDelivery(confirmed.get(0))), + assertTrue(java.util.Arrays.equals(HttpProtocolAccess.storedDelivery(delivery), + HttpProtocolAccess.storedDelivery(confirmed.get(0))), "a confirmed same-ID retry must become deliverable after restart"); - HttpProxyTransportServer.BackendState confirmedState = new HttpProxyTransportServer.BackendState( + HttpTestAccess.BackendState confirmedState = new HttpTestAccess.BackendState( "lobby-1", confirmedQueue, (server, id) -> { }); assertTrue(confirmedState.enqueue(delivery)); confirmedState.acknowledge(java.util.List.of(deliveryId)); @@ -347,20 +355,20 @@ void publishedOutgoingDeliveryRemainsTrackedUntilDurabilityCanBeConfirmed() thro @Test void acknowledgedBackendDropsItsEmptyOutgoingIndexes() throws Exception { Path queueRoot = directory.resolve("acknowledged-index-cleanup"); - try (HttpProxyTransportServer.DurableOutgoingQueue queue = new HttpProxyTransportServer.DurableOutgoingQueue( + try (HttpTestAccess.DurableOutgoingQueue queue = new HttpTestAccess.DurableOutgoingQueue( queueRoot, com.bencodez.simpleapi.file.DurableFiles::forceDirectory)) { - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState( + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState( "lobby-1", queue, (server, id) -> { }); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery( + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery( java.util.UUID.randomUUID().toString(), JsonEnvelope.builder("cleanup").build()); assertTrue(state.enqueue(delivery)); state.acknowledge(java.util.List.of(delivery.id())); - var quarantinedField = HttpProxyTransportServer.DurableOutgoingQueue.class + var quarantinedField = HttpTestAccess.DurableOutgoingQueue.TYPE .getDeclaredField("quarantinedFiles"); quarantinedField.setAccessible(true); @SuppressWarnings("unchecked") - var quarantined = (java.util.Map>) quarantinedField.get(queue); + var quarantined = (java.util.Map>) quarantinedField.get(queue.target()); assertFalse(quarantined.containsKey("lobby-1"), "a deleted backend queue must not retain an empty quarantine index"); } @@ -371,14 +379,14 @@ void unresolvedOutgoingPublicationDoesNotReportAFalseRejection() throws Exceptio AtomicLong forceCalls = new AtomicLong(); java.util.concurrent.atomic.AtomicBoolean failForces = new java.util.concurrent.atomic.AtomicBoolean(true); Path queueRoot = directory.resolve("unresolved-outgoing"); - HttpProxyTransportServer.DurableOutgoingQueue queue = new HttpProxyTransportServer.DurableOutgoingQueue( + HttpTestAccess.DurableOutgoingQueue queue = new HttpTestAccess.DurableOutgoingQueue( queueRoot, ignored -> { if (forceCalls.incrementAndGet() >= 3L && failForces.get()) throw new java.io.IOException("persistent directory force failure"); }); - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState( + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState( "lobby-1", queue, (server, id) -> { }); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery( + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery( java.util.UUID.randomUUID().toString(), JsonEnvelope.builder("unresolved").build()); assertThrows(IllegalStateException.class, () -> state.enqueue(delivery), "an indeterminate rollback must not be reported as a definitive false result"); @@ -386,7 +394,7 @@ void unresolvedOutgoingPublicationDoesNotReportAFalseRejection() throws Exceptio assertTrue(state.enqueue(delivery), "a same-ID retry must recover the observed quarantine"); assertEquals(1L, countRegularFiles(queueRoot), "recovery must not leave duplicate queue entries"); queue.close(); - HttpProxyTransportServer.DurableOutgoingQueue restarted = new HttpProxyTransportServer.DurableOutgoingQueue( + HttpTestAccess.DurableOutgoingQueue restarted = new HttpTestAccess.DurableOutgoingQueue( queueRoot, com.bencodez.simpleapi.file.DurableFiles::forceDirectory); assertEquals(1, restarted.load().get("lobby-1").size()); restarted.close(); @@ -397,10 +405,10 @@ void proxyInboundCompletionSurvivesRestartBeforeAcknowledgement() throws Excepti Path root = directory.resolve("proxy-incoming"); Files.createDirectory(root); String deliveryId = java.util.UUID.randomUUID().toString(); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(deliveryId, + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(deliveryId, JsonEnvelope.builder("backend-event").build()); - HttpInboundDeliveryStore firstStore = HttpInboundDeliveryStore.open(root, "lobby-1"); - HttpProxyTransportServer.BackendState first = new HttpProxyTransportServer.BackendState( + HttpInboundStoreAccess firstStore = HttpInboundStoreAccess.open(root, "lobby-1"); + HttpTestAccess.BackendState first = new HttpTestAccess.BackendState( "lobby-1", null, firstStore, (server, id) -> { }); assertEquals(java.util.List.of(delivery), first.acceptIncoming(java.util.List.of(delivery))); first.beginIncoming(deliveryId); @@ -408,18 +416,18 @@ void proxyInboundCompletionSurvivesRestartBeforeAcknowledgement() throws Excepti first.completeIncoming(deliveryId, true); firstStore.seal(); - HttpInboundDeliveryStore restartedStore = HttpInboundDeliveryStore.open(root, "lobby-1"); - HttpProxyTransportServer.BackendState restarted = new HttpProxyTransportServer.BackendState( + HttpInboundStoreAccess restartedStore = HttpInboundStoreAccess.open(root, "lobby-1"); + HttpTestAccess.BackendState restarted = new HttpTestAccess.BackendState( "lobby-1", null, restartedStore, (server, id) -> { }); assertTrue(restarted.acceptIncoming(java.util.List.of(delivery)).isEmpty(), "a completed callback must not run again after a lost response and proxy restart"); - HttpProxyTransportServer.Response response = restarted.await("lobby-1", + HttpTestAccess.Response response = restarted.await("lobby-1", java.util.UUID.randomUUID().toString(), 0); assertEquals(java.util.List.of(deliveryId), response.acks()); restarted.confirmIncoming(response.acks()); restartedStore.seal(); - HttpInboundDeliveryStore confirmedStore = HttpInboundDeliveryStore.open(root, "lobby-1"); - HttpProxyTransportServer.BackendState confirmed = new HttpProxyTransportServer.BackendState( + HttpInboundStoreAccess confirmedStore = HttpInboundStoreAccess.open(root, "lobby-1"); + HttpTestAccess.BackendState confirmed = new HttpTestAccess.BackendState( "lobby-1", null, confirmedStore, (server, id) -> { }); assertEquals(java.util.List.of(delivery), confirmed.acceptIncoming(java.util.List.of(delivery)), "only an acknowledgement confirmation may retire the durable replay fence"); @@ -430,14 +438,14 @@ void inboundJournalHasOneWriterWhileReadOnlyInspectionRemainsAvailable() throws Path root = directory.resolve("exclusive-incoming"); Files.createDirectory(root); String deliveryId = java.util.UUID.randomUUID().toString(); - HttpInboundDeliveryStore owner = HttpInboundDeliveryStore.open(root, "lobby-1"); + HttpInboundStoreAccess owner = HttpInboundStoreAccess.open(root, "lobby-1"); owner.reserve(deliveryId); - assertThrows(java.io.IOException.class, () -> HttpInboundDeliveryStore.open(root, "lobby-1"), + assertThrows(java.io.IOException.class, () -> HttpInboundStoreAccess.open(root, "lobby-1"), "a live journal owner must exclude a stale in-memory writer"); - assertEquals(HttpInboundDeliveryStore.State.RESERVED, - HttpInboundDeliveryStore.inspect(root, "lobby-1").state(deliveryId)); + assertEquals(HttpInboundStoreAccess.State.RESERVED, + HttpInboundStoreAccess.inspect(root, "lobby-1").state(deliveryId)); owner.seal(); - HttpInboundDeliveryStore successor = HttpInboundDeliveryStore.open(root, "lobby-1"); + HttpInboundStoreAccess successor = HttpInboundStoreAccess.open(root, "lobby-1"); successor.markRunning(deliveryId); successor.seal(); } @@ -455,7 +463,7 @@ void backendConnectorReleasesJournalOwnershipOnlyAfterClose() throws Exception { "two live connectors must not own the same inbound delivery journal"); } try (HttpBackendTransportConnector successor = new HttpBackendTransportConnector(credentials, ignored -> { })) { - assertFalse(successor.pollerAlive()); + assertFalse(HttpTestAccess.pollerAlive(successor)); } } @@ -466,10 +474,10 @@ void completedInboundPublicationRetriesForceBeforeProxyAcknowledgement() throws String deliveryId = java.util.UUID.randomUUID().toString(); Path inboundDirectory = root.resolve("lobby-1"); Path completed = inboundDirectory.resolve(deliveryId + ".completed"); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(deliveryId, + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(deliveryId, JsonEnvelope.builder("backend-event").build()); - HttpInboundDeliveryStore store = HttpInboundDeliveryStore.open(root, "lobby-1"); - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState( + HttpInboundStoreAccess store = HttpInboundStoreAccess.open(root, "lobby-1"); + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState( "lobby-1", null, store, (server, id) -> { }); try (var forces = org.mockito.Mockito.mockStatic(com.bencodez.simpleapi.file.DurableFiles.class, org.mockito.Mockito.CALLS_REAL_METHODS)) { @@ -483,7 +491,7 @@ void completedInboundPublicationRetriesForceBeforeProxyAcknowledgement() throws assertThrows(com.bencodez.simpleapi.file.DurableFiles.PublishedException.class, () -> state.completeIncomingDurably(deliveryId)); state.completeIncoming(deliveryId, false); - assertEquals(HttpInboundDeliveryStore.State.COMPLETED, store.state(deliveryId)); + assertEquals(HttpInboundStoreAccess.State.COMPLETED, store.state(deliveryId)); assertTrue(state.acceptIncoming(java.util.List.of(delivery)).isEmpty(), "a visible completed target must never rerun its callback while fsync is unresolved"); assertTrue(state.await("lobby-1", java.util.UUID.randomUUID().toString(), 0).acks().isEmpty(), @@ -504,9 +512,9 @@ void reservedInboundPublicationRetriesBeforeProxyCallback() throws Exception { CountDownLatch firstForce = new CountDownLatch(1), secondForce = new CountDownLatch(1); java.util.concurrent.atomic.AtomicInteger forceCalls = new java.util.concurrent.atomic.AtomicInteger(); java.util.concurrent.atomic.AtomicInteger callbacks = new java.util.concurrent.atomic.AtomicInteger(); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(deliveryId, JsonEnvelope.builder("vote").build()); - HttpInboundDeliveryStore store = HttpInboundDeliveryStore.open(root, "lobby-1"); - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState( + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(deliveryId, JsonEnvelope.builder("vote").build()); + HttpInboundStoreAccess store = HttpInboundStoreAccess.open(root, "lobby-1"); + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState( "lobby-1", null, store, (server, id) -> { }); try (var forces = org.mockito.Mockito.mockStatic(com.bencodez.simpleapi.file.DurableFiles.class, org.mockito.Mockito.CALLS_REAL_METHODS)) { @@ -546,7 +554,7 @@ void runningPublicationRollbackLeavesAReservationSafeAfterRestart() throws Excep String deliveryId = java.util.UUID.randomUUID().toString(); Path inboundDirectory = root.resolve("lobby-1"); Path running = inboundDirectory.resolve(deliveryId + ".running"); - HttpInboundDeliveryStore store = HttpInboundDeliveryStore.open(root, "lobby-1"); + HttpInboundStoreAccess store = HttpInboundStoreAccess.open(root, "lobby-1"); try (var forces = org.mockito.Mockito.mockStatic(com.bencodez.simpleapi.file.DurableFiles.class, org.mockito.Mockito.CALLS_REAL_METHODS)) { java.util.concurrent.atomic.AtomicBoolean failed = new java.util.concurrent.atomic.AtomicBoolean(); @@ -559,11 +567,11 @@ void runningPublicationRollbackLeavesAReservationSafeAfterRestart() throws Excep store.reserve(deliveryId); assertThrows(com.bencodez.simpleapi.file.DurableFiles.PublishedException.class, () -> store.markRunning(deliveryId)); - assertEquals(HttpInboundDeliveryStore.State.RESERVED, store.state(deliveryId)); + assertEquals(HttpInboundStoreAccess.State.RESERVED, store.state(deliveryId)); } store.seal(); - assertEquals(HttpInboundDeliveryStore.State.RESERVED, - HttpInboundDeliveryStore.inspect(root, "lobby-1").state(deliveryId), + assertEquals(HttpInboundStoreAccess.State.RESERVED, + HttpInboundStoreAccess.inspect(root, "lobby-1").state(deliveryId), "a callback never exposed must be recoverable after a one-shot RUNNING force failure"); } @@ -576,9 +584,9 @@ void proxyRetriesKnownNotStartedRunningRollbackBeforeDispatch() throws Exception Path running = inboundDirectory.resolve(deliveryId + ".running"); AtomicInteger failures = new AtomicInteger(2), callbacks = new AtomicInteger(); java.util.concurrent.atomic.AtomicBoolean rollbackStarted = new java.util.concurrent.atomic.AtomicBoolean(); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(deliveryId, JsonEnvelope.builder("vote").build()); - HttpInboundDeliveryStore store = HttpInboundDeliveryStore.open(root, "lobby-1"); - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState( + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(deliveryId, JsonEnvelope.builder("vote").build()); + HttpInboundStoreAccess store = HttpInboundStoreAccess.open(root, "lobby-1"); + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState( "lobby-1", null, store, (server, id) -> { }); try (var forces = org.mockito.Mockito.mockStatic(com.bencodez.simpleapi.file.DurableFiles.class, org.mockito.Mockito.CALLS_REAL_METHODS)) { @@ -616,11 +624,11 @@ void backendRetriesKnownNotStartedRunningRollbackBeforeDispatch() throws Excepti Path running = inboundDirectory.resolve(deliveryId + ".running"); AtomicInteger failures = new AtomicInteger(2), callbacks = new AtomicInteger(); java.util.concurrent.atomic.AtomicBoolean rollbackStarted = new java.util.concurrent.atomic.AtomicBoolean(); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(deliveryId, JsonEnvelope.builder("vote").build()); + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(deliveryId, JsonEnvelope.builder("vote").build()); try (var forces = org.mockito.Mockito.mockStatic(com.bencodez.simpleapi.file.DurableFiles.class, org.mockito.Mockito.CALLS_REAL_METHODS); HttpBackendTransportConnector connector = new HttpBackendTransportConnector(clientDirectory, - ignored -> callbacks.incrementAndGet())) { + ignored -> callbacks.incrementAndGet())) { forces.when(() -> com.bencodez.simpleapi.file.DurableFiles.forceDirectory(inboundDirectory)) .thenAnswer(call -> { if (Files.exists(running)) rollbackStarted.set(true); @@ -630,13 +638,13 @@ void backendRetriesKnownNotStartedRunningRollbackBeforeDispatch() throws Excepti }); var inboundField = HttpBackendTransportConnector.class.getDeclaredField("inboundDeliveries"); inboundField.setAccessible(true); - HttpInboundDeliveryStore store = (HttpInboundDeliveryStore) inboundField.get(connector); + HttpInboundStoreAccess store = new HttpInboundStoreAccess(inboundField.get(connector)); store.reserve(deliveryId); assertThrows(com.bencodez.simpleapi.file.DurableFiles.PublishedException.class, () -> store.markRunning(deliveryId)); - java.util.List retried = connector.accept(java.util.List.of(delivery)); + java.util.List retried = HttpTestAccess.accept(connector, java.util.List.of(delivery)); assertEquals(java.util.List.of(delivery), retried); - connector.dispatch(retried.get(0)); + HttpTestAccess.dispatch(connector, retried.get(0)); long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2); while (callbacks.get() == 0 && System.nanoTime() < deadline) Thread.sleep(5); assertEquals(1, callbacks.get()); @@ -645,10 +653,10 @@ void backendRetriesKnownNotStartedRunningRollbackBeforeDispatch() throws Excepti @Test void failedAcknowledgementCallbackRetainsProxyDelivery() throws Exception { - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState("lobby-1", null, + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState("lobby-1", null, (server, deliveryId) -> { throw new java.io.IOException("cache save failed"); }); String deliveryId = java.util.UUID.randomUUID().toString(); - assertTrue(state.enqueue(new HttpTransportProtocol.Delivery(deliveryId, + assertTrue(state.enqueue(new HttpProtocolAccess.Delivery(deliveryId, JsonEnvelope.builder("vote-party").build()))); assertThrows(java.io.IOException.class, () -> state.acknowledge(java.util.List.of(deliveryId))); assertEquals(deliveryId, state.await("lobby-1", java.util.UUID.randomUUID().toString(), 0) @@ -660,21 +668,21 @@ void oppositeDirectionIdsUseSeparateAcknowledgementNamespaces() throws Exception String deliveryId = java.util.UUID.randomUUID().toString(); Path incomingRoot = directory.resolve("separate-ack-incoming"); Files.createDirectory(incomingRoot); - HttpInboundDeliveryStore incoming = HttpInboundDeliveryStore.open(incomingRoot, "lobby-1"); - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState( + HttpInboundStoreAccess incoming = HttpInboundStoreAccess.open(incomingRoot, "lobby-1"); + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState( "lobby-1", null, incoming, (server, id) -> { }); - assertTrue(state.enqueue(new HttpTransportProtocol.Delivery(deliveryId, + assertTrue(state.enqueue(new HttpProtocolAccess.Delivery(deliveryId, JsonEnvelope.builder("proxy-reply").build()))); - HttpTransportProtocol.Delivery backendMessage = new HttpTransportProtocol.Delivery(deliveryId, + HttpProtocolAccess.Delivery backendMessage = new HttpProtocolAccess.Delivery(deliveryId, JsonEnvelope.builder("backend-request").build()); assertEquals(java.util.List.of(backendMessage), state.acceptIncoming(java.util.List.of(backendMessage))); state.beginIncoming(deliveryId); state.completeIncomingDurably(deliveryId); state.completeIncoming(deliveryId, true); - byte[] request = HttpTransportProtocol.request("lobby-1", java.util.UUID.randomUUID().toString(), 0, + byte[] request = HttpProtocolAccess.request("lobby-1", java.util.UUID.randomUUID().toString(), 0, java.util.List.of(), java.util.List.of(deliveryId), java.util.List.of()); - HttpTransportProtocol.Packet packet = HttpTransportProtocol.parsePacket(request); + HttpProtocolAccess.Packet packet = HttpProtocolAccess.parsePacket(request); state.confirmIncoming(packet.ackConfirmations()); state.acknowledge(packet.acks()); assertEquals(deliveryId, state.await("lobby-1", java.util.UUID.randomUUID().toString(), 0) @@ -686,7 +694,7 @@ void oppositeDirectionIdsUseSeparateAcknowledgementNamespaces() throws Exception void closeWaitsForTheCredentialOwningPollerToStop() throws Exception { HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("close-proxy"), "localhost"); HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("close-authority")); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, ignored -> { })) { server.start(); Path clientDirectory = directory.resolve("close-client"); @@ -697,7 +705,7 @@ void closeWaitsForTheCredentialOwningPollerToStop() throws Exception { connector.start(); assertTrue(connector.awaitFirstResponse(System.nanoTime() + TimeUnit.SECONDS.toNanos(8))); connector.close(); - assertFalse(connector.pollerAlive(), + assertFalse(HttpTestAccess.pollerAlive(connector), "credential-directory ownership must outlive every poller filesystem mutation"); } } @@ -718,7 +726,7 @@ void closeDrainsRunningCallbacksBeforeSealingTheirJournal() throws Exception { catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); } }); String id = java.util.UUID.randomUUID().toString(); - connector.dispatch(new HttpTransportProtocol.Delivery(id, JsonEnvelope.builder("close-callback").build())); + HttpTestAccess.dispatch(connector, new HttpProtocolAccess.Delivery(id, JsonEnvelope.builder("close-callback").build())); assertTrue(started.await(2, TimeUnit.SECONDS)); Thread closer = new Thread(connector::close, "close-callback-test"); closer.start(); @@ -728,8 +736,8 @@ void closeDrainsRunningCallbacksBeforeSealingTheirJournal() throws Exception { } finally { release.countDown(); } closer.join(3000); assertFalse(closer.isAlive()); - assertEquals(HttpInboundDeliveryStore.State.COMPLETED, - HttpInboundDeliveryStore.inspect(clientDirectory).state(id)); + assertEquals(HttpInboundStoreAccess.State.COMPLETED, + HttpInboundStoreAccess.inspect(clientDirectory).state(id)); } @Test @@ -748,7 +756,7 @@ void interruptedCloseRemainsBoundedWhenACallbackIgnoresInterruption() throws Exc stopped.countDown(); }); String id = java.util.UUID.randomUUID().toString(); - connector.dispatch(new HttpTransportProtocol.Delivery(id, JsonEnvelope.builder("bounded-close").build())); + HttpTestAccess.dispatch(connector, new HttpProtocolAccess.Delivery(id, JsonEnvelope.builder("bounded-close").build())); assertTrue(started.await(2, TimeUnit.SECONDS)); Thread closer = new Thread(connector::close, "bounded-close-test"); closer.start(); @@ -761,13 +769,13 @@ void interruptedCloseRemainsBoundedWhenACallbackIgnoresInterruption() throws Exc } finally { release.countDown(); } connector.close(); long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2); - HttpInboundDeliveryStore.State state; + Object state; do { - state = HttpInboundDeliveryStore.inspect(clientDirectory).state(id); - if (state == HttpInboundDeliveryStore.State.COMPLETED) break; + state = HttpInboundStoreAccess.inspect(clientDirectory).state(id); + if (state == HttpInboundStoreAccess.State.COMPLETED) break; Thread.sleep(10); } while (System.nanoTime() < deadline); - assertEquals(HttpInboundDeliveryStore.State.COMPLETED, state, + assertEquals(HttpInboundStoreAccess.State.COMPLETED, state, "the deferred journal seal must allow the late callback completion to become durable"); } @@ -785,7 +793,7 @@ void proxyShutdownGivesInterruptedCallbacksTimeToFinish() throws Exception { } finally { finished.countDown(); } }); assertTrue(started.await(2, TimeUnit.SECONDS)); - Thread closer = new Thread(() -> HttpProxyTransportServer.shutdown(executor), "proxy-shutdown-test"); + Thread closer = new Thread(() -> HttpTestAccess.shutdown(executor), "proxy-shutdown-test"); closer.start(); closer.interrupt(); closer.join(2500); @@ -800,7 +808,7 @@ void finalFlushDeliversMessagesQueuedBehindAnActiveLongPoll() throws Exception { HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("flush-authority")); CountDownLatch received = new CountDownLatch(1); AtomicReference envelope = new AtomicReference<>(); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, message -> { envelope.set(message.envelope()); received.countDown(); @@ -818,7 +826,7 @@ void finalFlushDeliversMessagesQueuedBehindAnActiveLongPoll() throws Exception { assertTrue(connector.flushOutgoing(System.nanoTime() + TimeUnit.SECONDS.toNanos(5))); assertTrue(received.await(1, TimeUnit.SECONDS)); assertEquals("backend-stopped", envelope.get().getSubChannel()); - assertEquals(0, connector.queuedOutgoing()); + assertEquals(0, HttpTestAccess.queuedOutgoing(connector)); } finally { connector.close(); } } } @@ -827,11 +835,11 @@ void finalFlushDeliversMessagesQueuedBehindAnActiveLongPoll() throws Exception { void normalTransportRejectsAClientWithoutCertificate() throws Exception { HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("proxy"), "localhost"); HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("authority")); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), identity, authority, ignored -> { })) { + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, ignored -> { })) { server.start(); HttpConnectionCode code = authority.createConnectionCode("lobby-1", server.endpoint("localhost"), Duration.ofMinutes(5)); HttpClient client = HttpClient.newBuilder().sslContext(HttpPinnedTls.clientContext(code)).build(); - byte[] body = HttpTransportProtocol.request("lobby-1", java.util.UUID.randomUUID().toString(), 0, + byte[] body = HttpProtocolAccess.request("lobby-1", java.util.UUID.randomUUID().toString(), 0, java.util.List.of(), java.util.List.of(), java.util.List.of()); HttpResponse response = client.send(HttpRequest.newBuilder(code.endpoint().resolve("v1/transport")) .timeout(Duration.ofSeconds(5)).header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofByteArray(body)).build(), HttpResponse.BodyHandlers.ofByteArray()); @@ -842,17 +850,17 @@ void normalTransportRejectsAClientWithoutCertificate() throws Exception { @Test void fixedRequestBodiesReportProtocolErrorsBeforeAdmissionErrors() { Headers headers = new Headers(); - assertEquals(411, HttpProxyTransportServer.fixedBodyErrorStatus(headers, 128)); + assertEquals(411, HttpTestAccess.fixedBodyErrorStatus(headers, 128)); headers.set("Content-Length", "invalid"); - assertEquals(400, HttpProxyTransportServer.fixedBodyErrorStatus(headers, 128)); + assertEquals(400, HttpTestAccess.fixedBodyErrorStatus(headers, 128)); headers.set("Content-Length", "0"); - assertEquals(400, HttpProxyTransportServer.fixedBodyErrorStatus(headers, 128)); + assertEquals(400, HttpTestAccess.fixedBodyErrorStatus(headers, 128)); headers.set("Content-Length", "129"); - assertEquals(413, HttpProxyTransportServer.fixedBodyErrorStatus(headers, 128)); + assertEquals(413, HttpTestAccess.fixedBodyErrorStatus(headers, 128)); headers.set("Content-Length", "128"); - assertEquals(0, HttpProxyTransportServer.fixedBodyErrorStatus(headers, 128)); + assertEquals(0, HttpTestAccess.fixedBodyErrorStatus(headers, 128)); headers.set("Transfer-Encoding", "chunked"); - assertEquals(400, HttpProxyTransportServer.fixedBodyErrorStatus(headers, 128)); + assertEquals(400, HttpTestAccess.fixedBodyErrorStatus(headers, 128)); } @Test @@ -860,7 +868,7 @@ void enrollmentPersistenceFailuresReturnServiceUnavailable() throws Exception { HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("failed-enroll-proxy"), "localhost"); Path authorityDirectory = directory.resolve("failed-enroll-authority"); HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, authorityDirectory); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, ignored -> { })) { server.start(); HttpConnectionCode code = authority.createConnectionCode("lobby-1", server.endpoint("localhost"), @@ -882,10 +890,10 @@ void enrollmentPersistenceFailuresReturnServiceUnavailable() throws Exception { void boundedQueuesFailClosed() throws Exception { HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("proxy"), "localhost"); HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("authority")); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), identity, authority, ignored -> { })) { - for (int i = 0; i < HttpTransportProtocol.MAX_QUEUE; i++) assertTrue(server.send("lobby-1", JsonEnvelope.builder("x").build())); + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, ignored -> { })) { + for (int i = 0; i < HttpProtocolAccess.MAX_QUEUE; i++) assertTrue(server.send("lobby-1", JsonEnvelope.builder("x").build())); assertFalse(server.send("lobby-1", JsonEnvelope.builder("x").build())); - assertFalse(server.send("lobby-1", JsonEnvelope.builder("x").put("large", "x".repeat(HttpTransportProtocol.MAX_ENVELOPE_BYTES)).build())); + assertFalse(server.send("lobby-1", JsonEnvelope.builder("x").put("large", "x".repeat(HttpProtocolAccess.MAX_ENVELOPE_BYTES)).build())); } } @@ -894,7 +902,7 @@ void proxyBackendStateIsGloballyBounded() throws Exception { HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("bounded-backend-proxy"), "localhost"); HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("bounded-backend-authority")); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, ignored -> { })) { for (int index = 0; index < 128; index++) assertTrue(server.send("server-" + index, JsonEnvelope.builder("x").build())); @@ -910,19 +918,19 @@ void proxyReclaimsOnlyQuiescentBackendStateAfterReplayWindow() throws Exception HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("reclaim-authority")); AtomicLong nanoTime = new AtomicLong(); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, null, ignored -> { }, (serverId, deliveryId) -> { }, nanoTime::get)) { for (int index = 0; index < 128; index++) { - HttpProxyTransportServer.BackendState state = server.backendStateForTest("server-" + index); + HttpTestAccess.BackendState state = HttpTestAccess.backendStateForTest(server, "server-" + index); assertTrue(state.beginPollForTest()); state.endPollForTest(); } assertFalse(server.send("replacement", JsonEnvelope.builder("x").build()), "fresh state must retain its replay fence"); - nanoTime.addAndGet(TimeUnit.MILLISECONDS.toNanos(HttpTransportProtocol.MAX_CLOCK_SKEW_MILLIS) + 1L); + nanoTime.addAndGet(TimeUnit.MILLISECONDS.toNanos(HttpProtocolAccess.MAX_CLOCK_SKEW_MILLIS) + 1L); assertTrue(server.send("replacement", JsonEnvelope.builder("x").build()), "a quiescent state must be reclaimable after captured requests expire"); - assertEquals(128, server.backendCountForTest()); + assertEquals(128, HttpTestAccess.backendCountForTest(server)); } } @@ -931,13 +939,13 @@ void proxyDoesNotReclaimAQuarantineOnlyBackendAfterReplayWindow() throws Excepti Path queueRoot = directory.resolve("quarantine-reclamation-outgoing"); AtomicLong forceCalls = new AtomicLong(); String serverId = "quarantined"; - HttpTransportProtocol.Delivery retry = new HttpTransportProtocol.Delivery( + HttpProtocolAccess.Delivery retry = new HttpProtocolAccess.Delivery( java.util.UUID.randomUUID().toString(), JsonEnvelope.builder("retry").build()); - try (HttpProxyTransportServer.DurableOutgoingQueue queue = new HttpProxyTransportServer.DurableOutgoingQueue( + try (HttpTestAccess.DurableOutgoingQueue queue = new HttpTestAccess.DurableOutgoingQueue( queueRoot, ignored -> { if (forceCalls.incrementAndGet() == 3L) throw new java.io.IOException("injected directory force failure"); })) { - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState( + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState( serverId, queue, (server, id) -> { }); assertFalse(state.enqueue(retry)); } @@ -946,18 +954,18 @@ void proxyDoesNotReclaimAQuarantineOnlyBackendAfterReplayWindow() throws Excepti HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("quarantine-reclamation-authority")); AtomicLong nanoTime = new AtomicLong(); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, queueRoot, ignored -> { }, (backend, delivery) -> { }, nanoTime::get)) { for (int index = 0; index < 127; index++) { - HttpProxyTransportServer.BackendState state = server.backendStateForTest("idle-" + index); + HttpTestAccess.BackendState state = HttpTestAccess.backendStateForTest(server, "idle-" + index); assertTrue(state.beginPollForTest()); state.endPollForTest(); } - nanoTime.addAndGet(TimeUnit.MILLISECONDS.toNanos(HttpTransportProtocol.MAX_CLOCK_SKEW_MILLIS) + 1L); + nanoTime.addAndGet(TimeUnit.MILLISECONDS.toNanos(HttpProtocolAccess.MAX_CLOCK_SKEW_MILLIS) + 1L); for (int index = 0; index < 127; index++) assertTrue(server.send("replacement-" + index, JsonEnvelope.builder("replacement").build())); assertFalse(server.send("overflow", JsonEnvelope.builder("overflow").build())); - assertTrue(server.backendStateForTest(serverId).enqueue(retry), + assertTrue(HttpTestAccess.backendStateForTest(server, serverId).enqueue(retry), "the identical retry must still reach and promote its quarantined delivery at capacity"); } } @@ -969,8 +977,8 @@ void proxyOutgoingQueueSurvivesRestartUntilBackendAcknowledges() throws Exceptio Path queueDirectory = directory.resolve("outgoing"); HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(proxyDirectory, "localhost"); HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, authorityDirectory); - HttpProxyTransportServer first = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), - identity, authority, queueDirectory, ignored -> { }); + HttpProxyTransportServer first = new HttpProxyTransportServer( + new InetSocketAddress("localhost", 0), identity, authority, queueDirectory, ignored -> { }); assertTrue(first.send("lobby-1", JsonEnvelope.builder("durable").build())); first.close(); @@ -982,7 +990,7 @@ void proxyOutgoingQueueSurvivesRestartUntilBackendAcknowledges() throws Exceptio Duration.ofMinutes(5)); HttpClientCredentialStore.ClientCredential credential = HttpBackendTransportConnector.enroll(code, "lobby-1", directory.resolve("durable-client")); - try (HttpBackendTransportConnector connector = new HttpBackendTransportConnector(code, "lobby-1", + try (HttpBackendTransportConnector connector = HttpTestAccess.connector(code, "lobby-1", credential, envelope -> received.countDown())) { connector.start(); assertTrue(received.await(8, TimeUnit.SECONDS)); @@ -1007,7 +1015,7 @@ void pollCreatedBackendStateUsesDurableOutgoingQueue() throws Exception { Duration.ofMinutes(5)); HttpClientCredentialStore.ClientCredential credential = HttpBackendTransportConnector.enroll(code, "lobby-1", directory.resolve("poll-client")); - try (HttpBackendTransportConnector connector = new HttpBackendTransportConnector(code, "lobby-1", + try (HttpBackendTransportConnector connector = HttpTestAccess.connector(code, "lobby-1", credential, envelope -> { backendReceived.countDown(); try { releaseBackendCallback.await(5, TimeUnit.SECONDS); } @@ -1035,23 +1043,23 @@ private static long countRegularFiles(Path root) throws Exception { @Test void aggregatePacketBudgetSplitsLargeValidEnvelopes() { - java.util.List candidates = new java.util.ArrayList<>(); - for (int index = 0; index < 12; index++) candidates.add(new HttpTransportProtocol.Delivery( + java.util.List candidates = new java.util.ArrayList<>(); + for (int index = 0; index < 12; index++) candidates.add(new HttpProtocolAccess.Delivery( java.util.UUID.randomUUID().toString(), JsonEnvelope.builder("large").put("value", "x".repeat(40_000)).build())); String session = java.util.UUID.randomUUID().toString(); - java.util.List fitted = HttpTransportProtocol.fittingMessages( + java.util.List fitted = HttpProtocolAccess.fittingMessages( "lobby-1", session, 0, java.util.List.of(), java.util.List.of(), candidates); assertTrue(fitted.size() > 0 && fitted.size() < candidates.size()); - assertTrue(HttpTransportProtocol.request("lobby-1", session, 0, java.util.List.of(), java.util.List.of(), fitted).length - <= HttpTransportProtocol.MAX_BODY_BYTES); + assertTrue(HttpProtocolAccess.request("lobby-1", session, 0, java.util.List.of(), java.util.List.of(), fitted).length + <= HttpProtocolAccess.MAX_BODY_BYTES); } @Test void packetNumbersMustUseCanonicalJsonIntegerTokens() { - com.google.gson.JsonObject packet = com.google.gson.JsonParser.parseString(new String(HttpTransportProtocol.request( + com.google.gson.JsonObject packet = com.google.gson.JsonParser.parseString(new String(HttpProtocolAccess.request( "lobby-1", java.util.UUID.randomUUID().toString(), 0, java.util.List.of(), java.util.List.of(), java.util.List.of()), java.nio.charset.StandardCharsets.UTF_8)).getAsJsonObject(); - assertDoesNotThrow(() -> HttpTransportProtocol.parsePacket(packet.toString() + assertDoesNotThrow(() -> HttpProtocolAccess.parsePacket(packet.toString() .getBytes(java.nio.charset.StandardCharsets.UTF_8))); String timestamp = packet.get("timestamp").getAsString(); java.util.Map> invalid = java.util.Map.of( @@ -1061,7 +1069,7 @@ void packetNumbersMustUseCanonicalJsonIntegerTokens() { for (var field : invalid.entrySet()) for (String token : field.getValue()) { com.google.gson.JsonObject rejected = packet.deepCopy(); rejected.add(field.getKey(), com.google.gson.JsonParser.parseString(token)); - assertThrows(IllegalArgumentException.class, () -> HttpTransportProtocol.parsePacket( + assertThrows(IllegalArgumentException.class, () -> HttpProtocolAccess.parsePacket( rejected.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8)), field.getKey() + "=" + token); } } @@ -1069,18 +1077,18 @@ void packetNumbersMustUseCanonicalJsonIntegerTokens() { @Test void protocolParsingRejectsDuplicateObjectMembers() { String deliveryId = java.util.UUID.randomUUID().toString(); - String packet = new String(HttpTransportProtocol.request("lobby-1", java.util.UUID.randomUUID().toString(), 0, - java.util.List.of(), java.util.List.of(), java.util.List.of(new HttpTransportProtocol.Delivery( + String packet = new String(HttpProtocolAccess.request("lobby-1", java.util.UUID.randomUUID().toString(), 0, + java.util.List.of(), java.util.List.of(), java.util.List.of(new HttpProtocolAccess.Delivery( deliveryId, JsonEnvelope.builder("payload").build()))), java.nio.charset.StandardCharsets.UTF_8); String duplicateRoot = packet.replaceFirst("\\\"server\\\":\\\"lobby-1\\\"", "\\\"server\\\":\\\"other\\\",\\\"server\\\":\\\"lobby-1\\\""); - assertThrows(IllegalArgumentException.class, () -> HttpTransportProtocol.parsePacket( + assertThrows(IllegalArgumentException.class, () -> HttpProtocolAccess.parsePacket( duplicateRoot.getBytes(java.nio.charset.StandardCharsets.UTF_8))); String duplicateNested = packet.replaceFirst("\\\"id\\\":\\\"" + deliveryId + "\\\"", "\\\"id\\\":\\\"" + deliveryId + "\\\",\\\"id\\\":\\\"" + deliveryId + "\\\""); - assertThrows(IllegalArgumentException.class, () -> HttpTransportProtocol.parsePacket( + assertThrows(IllegalArgumentException.class, () -> HttpProtocolAccess.parsePacket( duplicateNested.getBytes(java.nio.charset.StandardCharsets.UTF_8))); - assertThrows(IllegalArgumentException.class, () -> HttpTransportProtocol.parseEnrollment( + assertThrows(IllegalArgumentException.class, () -> HttpProtocolAccess.parseEnrollment( ("{\\\"server\\\":\\\"lobby-1\\\",\\\"server\\\":\\\"lobby-2\\\",\\\"token\\\":\\\"" + "A".repeat(43) + "\\\"}").getBytes(java.nio.charset.StandardCharsets.UTF_8))); } @@ -1088,30 +1096,30 @@ void protocolParsingRejectsDuplicateObjectMembers() { @Test void packetParsingRejectsNoncanonicalUuidForms() { String deliveryId = java.util.UUID.randomUUID().toString(); - com.google.gson.JsonObject packet = com.google.gson.JsonParser.parseString(new String(HttpTransportProtocol.request( + com.google.gson.JsonObject packet = com.google.gson.JsonParser.parseString(new String(HttpProtocolAccess.request( "lobby-1", java.util.UUID.randomUUID().toString(), 0, java.util.List.of(deliveryId), java.util.List.of(deliveryId), - java.util.List.of(new HttpTransportProtocol.Delivery(deliveryId, JsonEnvelope.builder("payload").build()))), + java.util.List.of(new HttpProtocolAccess.Delivery(deliveryId, JsonEnvelope.builder("payload").build()))), java.nio.charset.StandardCharsets.UTF_8)).getAsJsonObject(); String abbreviated = "1-1-1-1-1"; com.google.gson.JsonObject invalidSession = packet.deepCopy(); invalidSession.addProperty("session", abbreviated); - assertThrows(IllegalArgumentException.class, () -> HttpTransportProtocol.parsePacket( + assertThrows(IllegalArgumentException.class, () -> HttpProtocolAccess.parsePacket( invalidSession.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8))); com.google.gson.JsonObject invalidAck = packet.deepCopy(); invalidAck.getAsJsonArray("acks").set(0, new com.google.gson.JsonPrimitive(abbreviated)); - assertThrows(IllegalArgumentException.class, () -> HttpTransportProtocol.parsePacket( + assertThrows(IllegalArgumentException.class, () -> HttpProtocolAccess.parsePacket( invalidAck.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8))); com.google.gson.JsonObject invalidConfirmation = packet.deepCopy(); invalidConfirmation.getAsJsonArray("ackConfirmations").set(0, new com.google.gson.JsonPrimitive(abbreviated)); - assertThrows(IllegalArgumentException.class, () -> HttpTransportProtocol.parsePacket( + assertThrows(IllegalArgumentException.class, () -> HttpProtocolAccess.parsePacket( invalidConfirmation.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8))); com.google.gson.JsonObject invalidMessage = packet.deepCopy(); invalidMessage.getAsJsonArray("messages").get(0).getAsJsonObject().addProperty("id", abbreviated); - assertThrows(IllegalArgumentException.class, () -> HttpTransportProtocol.parsePacket( + assertThrows(IllegalArgumentException.class, () -> HttpProtocolAccess.parsePacket( invalidMessage.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8))); } @@ -1119,7 +1127,7 @@ void packetParsingRejectsNoncanonicalUuidForms() { void persistedProfileStartsAfterTheEnrollmentCodeExpires() throws Exception { HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("proxy"), "localhost"); HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("authority")); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), identity, authority, ignored -> { })) { + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, ignored -> { })) { server.start(); HttpConnectionCode active = authority.createConnectionCode("lobby-1", server.endpoint("localhost"), Duration.ofMinutes(5)); HttpBackendTransportConnector.enroll(active, "lobby-1", directory.resolve("client")); @@ -1138,13 +1146,13 @@ void persistedProfileStartsAfterTheEnrollmentCodeExpires() throws Exception { void persistedBackendConnectsAfterAutomaticServerLeafRotation() throws Exception { Instant now = Instant.now(); Path proxyDirectory = directory.resolve("proxy"); - HttpTlsIdentity original = HttpTlsIdentity.loadOrCreate(proxyDirectory, "localhost", + HttpTlsIdentity original = HttpTestAccess.identity(proxyDirectory, "localhost", java.time.Clock.fixed(now.minus(Duration.ofDays(340)), java.time.ZoneOffset.UTC)); - String originalServerPin = HttpTransportSecrets.certificatePin(original.serverCertificate()); + String originalServerPin = HttpSecretsAccess.certificatePin(original.serverCertificate()); HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(original, directory.resolve("authority")); HttpTlsIdentity rotated = HttpTlsIdentity.loadOrCreate(proxyDirectory, "localhost"); CountDownLatch received = new CountDownLatch(1); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), rotated, + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), rotated, authority, ignored -> received.countDown())) { HttpConnectionCode activeCode = authority.createConnectionCode("lobby-1", server.endpoint("localhost"), Duration.ofMinutes(5)); HttpTlsIdentity.IssuedClientCertificate issued = authority.enroll("lobby-1", activeCode.enrollmentToken()); @@ -1164,9 +1172,9 @@ void persistedBackendConnectsAfterAutomaticServerLeafRotation() throws Exception @Test void renewalRetainsOldCredentialUntilPublishedPointerIsConfirmed() throws Exception { HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("pointer-proxy"), "localhost"); - HttpTlsIdentity.IssuedClientCertificate expiring = identity.issueClientCertificate("lobby-1", + HttpTlsIdentity.IssuedClientCertificate expiring = HttpTestAccess.issueClientCertificate(identity, "lobby-1", Instant.now().minus(Duration.ofDays(340))); - String originalPin = HttpTransportSecrets.certificatePin(expiring.certificate()); + String originalPin = HttpSecretsAccess.certificatePin(expiring.certificate()); Path authorityDirectory = Files.createDirectory(directory.resolve("pointer-authority")); String key = java.util.Base64.getUrlEncoder().withoutPadding() .encodeToString("lobby-1".getBytes(java.nio.charset.StandardCharsets.UTF_8)); @@ -1174,7 +1182,7 @@ void renewalRetainsOldCredentialUntilPublishedPointerIsConfirmed() throws Except "version=2\nbinding." + key + "=" + originalPin + ":-:0\n"); HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, authorityDirectory); CountDownLatch received = new CountDownLatch(1); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, ignored -> received.countDown())) { Path clientDirectory = directory.resolve("pointer-client"); HttpConnectionCode code = new HttpConnectionCode("lobby-1", server.endpoint("localhost"), @@ -1204,9 +1212,9 @@ void renewalRetainsOldCredentialUntilPublishedPointerIsConfirmed() throws Except renew.invoke(connector); generation = Files.readString(pointer); var selected = HttpClientCredentialStore.load(clientDirectory); - String selectedPin = HttpTransportSecrets.certificatePin(selected.certificate()); + String selectedPin = HttpSecretsAccess.certificatePin(selected.certificate()); assertFalse(originalPin.equals(selectedPin)); - assertEquals(originalPin, HttpTransportSecrets.certificatePin( + assertEquals(originalPin, HttpSecretsAccess.certificatePin( ((HttpClientCredentialStore.ClientCredential) credential.get(connector)).certificate())); assertTrue(pending.get(connector) != null); assertTrue(authority.authenticate("lobby-1", expiring.certificate()), @@ -1219,8 +1227,8 @@ void renewalRetainsOldCredentialUntilPublishedPointerIsConfirmed() throws Except assertTrue(pending.get(connector) == null); assertEquals(generation, Files.readString(pointer)); var confirmed = HttpClientCredentialStore.load(clientDirectory); - assertEquals(HttpTransportSecrets.certificatePin(confirmed.certificate()), - HttpTransportSecrets.certificatePin(((HttpClientCredentialStore.ClientCredential) + assertEquals(HttpSecretsAccess.certificatePin(confirmed.certificate()), + HttpSecretsAccess.certificatePin(((HttpClientCredentialStore.ClientCredential) credential.get(connector)).certificate())); assertTrue(authority.authenticate("lobby-1", confirmed.certificate())); assertFalse(authority.authenticate("lobby-1", expiring.certificate())); @@ -1234,9 +1242,9 @@ void renewalRetainsOldCredentialUntilPublishedPointerIsConfirmed() throws Except @Test void backendRenewsClientCertificateBeforeExpiryWithoutNewConnectionCode() throws Exception { HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("proxy"), "localhost"); - HttpTlsIdentity.IssuedClientCertificate expiring = identity.issueClientCertificate("lobby-1", + HttpTlsIdentity.IssuedClientCertificate expiring = HttpTestAccess.issueClientCertificate(identity, "lobby-1", Instant.now().minus(Duration.ofDays(340))); - String originalPin = HttpTransportSecrets.certificatePin(expiring.certificate()); + String originalPin = HttpSecretsAccess.certificatePin(expiring.certificate()); Path authorityDirectory = directory.resolve("authority"); java.nio.file.Files.createDirectories(authorityDirectory); String key = java.util.Base64.getUrlEncoder().withoutPadding() @@ -1244,7 +1252,7 @@ void backendRenewsClientCertificateBeforeExpiryWithoutNewConnectionCode() throws java.nio.file.Files.writeString(authorityDirectory.resolve("http-transport-clients.properties"), "version=2\nbinding." + key + "=" + originalPin + ":-:0\n"); HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, authorityDirectory); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), identity, + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, ignored -> { })) { HttpConnectionCode profileCode = new HttpConnectionCode("lobby-1", server.endpoint("localhost"), identity.serverCertificatePin(), identity.caCertificatePin(), Instant.now().plusSeconds(60), "A".repeat(43)); @@ -1257,7 +1265,7 @@ void backendRenewsClientCertificateBeforeExpiryWithoutNewConnectionCode() throws String renewedPin = originalPin; while (renewedPin.equals(originalPin) && System.nanoTime() < deadline) { Thread.sleep(25); - renewedPin = HttpTransportSecrets.certificatePin(HttpClientCredentialStore.load(clientDirectory).certificate()); + renewedPin = HttpSecretsAccess.certificatePin(HttpClientCredentialStore.load(clientDirectory).certificate()); } assertFalse(renewedPin.equals(originalPin)); HttpClientCredentialStore.ClientCredential renewed = HttpClientCredentialStore.load(clientDirectory); @@ -1271,10 +1279,10 @@ void backendRenewsClientCertificateBeforeExpiryWithoutNewConnectionCode() throws @Test void duplicateInboundDeliveryIsReAcknowledgedWithoutSecondDispatch() { - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState(); + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState(); String session = java.util.UUID.randomUUID().toString(); String id = java.util.UUID.randomUUID().toString(); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(id, JsonEnvelope.builder("x").build()); + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(id, JsonEnvelope.builder("x").build()); assertTrue(state.acceptSession(session, 0)); assertEquals(1, state.acceptIncoming(java.util.List.of(delivery)).size()); state.completeIncoming(id, true); @@ -1291,11 +1299,11 @@ void duplicateInboundDeliveryIsReAcknowledgedWithoutSecondDispatch() { @Test void newerDeliveriesDoNotPostponeRetryOfOlderUnacknowledgedDelivery() { AtomicLong nanoTime = new AtomicLong(1L); - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState(nanoTime::get); + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState(nanoTime::get); String session = java.util.UUID.randomUUID().toString(); - HttpTransportProtocol.Delivery first = new HttpTransportProtocol.Delivery( + HttpProtocolAccess.Delivery first = new HttpProtocolAccess.Delivery( java.util.UUID.randomUUID().toString(), JsonEnvelope.builder("first").build()); - HttpTransportProtocol.Delivery second = new HttpTransportProtocol.Delivery( + HttpProtocolAccess.Delivery second = new HttpProtocolAccess.Delivery( java.util.UUID.randomUUID().toString(), JsonEnvelope.builder("second").build()); assertTrue(state.acceptSession(session, 0)); @@ -1316,17 +1324,17 @@ void newerDeliveriesDoNotPostponeRetryOfOlderUnacknowledgedDelivery() { @Test void longPollWakesAtTheOldestDeliveryRetryDeadline() throws Exception { AtomicLong nanoTime = new AtomicLong(1L); - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState(nanoTime::get); + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState(nanoTime::get); String session = java.util.UUID.randomUUID().toString(); - HttpTransportProtocol.Delivery first = new HttpTransportProtocol.Delivery( + HttpProtocolAccess.Delivery first = new HttpProtocolAccess.Delivery( java.util.UUID.randomUUID().toString(), JsonEnvelope.builder("first").build()); - HttpTransportProtocol.Delivery second = new HttpTransportProtocol.Delivery( + HttpProtocolAccess.Delivery second = new HttpProtocolAccess.Delivery( java.util.UUID.randomUUID().toString(), JsonEnvelope.builder("second").build()); assertTrue(state.acceptSession(session, 0)); assertTrue(state.enqueue(first)); assertEquals(java.util.List.of(first), state.await("lobby-1", session, 0).messages()); - nanoTime.addAndGet(HttpProxyTransportServer.LONG_POLL.minusMillis(100).toNanos()); + nanoTime.addAndGet(HttpTestAccess.LONG_POLL.minusMillis(100).toNanos()); assertTrue(state.acceptSession(session, 1)); assertTrue(state.enqueue(second)); assertEquals(java.util.List.of(second), state.await("lobby-1", session, 1).messages()); @@ -1347,18 +1355,18 @@ void longPollWakesAtTheOldestDeliveryRetryDeadline() throws Exception { @Test void proxyDedupWindowEvictsOldestCompletedDeliveryAtCapacity() { - HttpProxyTransportServer.BackendState state = new HttpProxyTransportServer.BackendState(); + HttpTestAccess.BackendState state = new HttpTestAccess.BackendState(); String oldest = null, newest = null; - for (int index = 0; index < HttpTransportProtocol.MAX_QUEUE + 1; index++) { + for (int index = 0; index < HttpProtocolAccess.MAX_QUEUE + 1; index++) { String id = java.util.UUID.randomUUID().toString(); if (index == 0) oldest = id; newest = id; - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(id, JsonEnvelope.builder("x").build()); + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(id, JsonEnvelope.builder("x").build()); assertEquals(1, state.acceptIncoming(java.util.List.of(delivery)).size()); state.completeIncoming(id, true); } - HttpTransportProtocol.Delivery evicted = new HttpTransportProtocol.Delivery(oldest, JsonEnvelope.builder("x").build()); - HttpTransportProtocol.Delivery retained = new HttpTransportProtocol.Delivery(newest, JsonEnvelope.builder("x").build()); + HttpProtocolAccess.Delivery evicted = new HttpProtocolAccess.Delivery(oldest, JsonEnvelope.builder("x").build()); + HttpProtocolAccess.Delivery retained = new HttpProtocolAccess.Delivery(newest, JsonEnvelope.builder("x").build()); assertEquals(1, state.acceptIncoming(java.util.List.of(evicted)).size()); assertTrue(state.acceptIncoming(java.util.List.of(retained)).isEmpty()); } @@ -1371,21 +1379,21 @@ void backendReAcknowledgesLostAckDuplicateWithoutSecondCallback() throws Excepti HttpClientCredentialStore.HttpClientProfile profile = new HttpClientCredentialStore.HttpClientProfile("lobby-1", java.net.URI.create("https://localhost:8443/"), identity.serverCertificatePin(), identity.caCertificatePin()); CountDownLatch callback = new CountDownLatch(1); - try (HttpBackendTransportConnector connector = new HttpBackendTransportConnector(profile, + try (HttpBackendTransportConnector connector = HttpTestAccess.connector(profile, HttpClientCredentialStore.load(directory.resolve("client")), envelope -> callback.countDown())) { String id = java.util.UUID.randomUUID().toString(); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(id, JsonEnvelope.builder("x").build()); - connector.dispatch(delivery); + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(id, JsonEnvelope.builder("x").build()); + HttpTestAccess.dispatch(connector, delivery); assertTrue(callback.await(2, TimeUnit.SECONDS)); java.util.List acknowledgements = java.util.List.of(); long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2); while (acknowledgements.isEmpty() && System.nanoTime() < deadline) { - acknowledgements = connector.drainAcknowledgements(); + acknowledgements = HttpTestAccess.drainAcknowledgements(connector); if (acknowledgements.isEmpty()) Thread.sleep(5); } assertEquals(java.util.List.of(id), acknowledgements); - assertTrue(connector.accept(java.util.List.of(delivery)).isEmpty()); - assertEquals(java.util.List.of(id), connector.drainAcknowledgements()); + assertTrue(HttpTestAccess.accept(connector, java.util.List.of(delivery)).isEmpty()); + assertEquals(java.util.List.of(id), HttpTestAccess.drainAcknowledgements(connector)); } } @@ -1398,7 +1406,7 @@ void backendCallbacksAreSerializedInDeliveryOrder() throws Exception { java.net.URI.create("https://localhost:8443/"), identity.serverCertificatePin(), identity.caCertificatePin()); CountDownLatch firstStarted = new CountDownLatch(1), releaseFirst = new CountDownLatch(1), secondStarted = new CountDownLatch(1); java.util.List order = new java.util.concurrent.CopyOnWriteArrayList<>(); - try (HttpBackendTransportConnector connector = new HttpBackendTransportConnector(profile, + try (HttpBackendTransportConnector connector = HttpTestAccess.connector(profile, HttpClientCredentialStore.load(directory.resolve("ordered-client")), envelope -> { String marker = String.valueOf(envelope.getFields().get("marker")); order.add(marker); @@ -1408,9 +1416,9 @@ void backendCallbacksAreSerializedInDeliveryOrder() throws Exception { catch (InterruptedException interrupted) { Thread.currentThread().interrupt(); } } else secondStarted.countDown(); })) { - connector.dispatch(new HttpTransportProtocol.Delivery(java.util.UUID.randomUUID().toString(), + HttpTestAccess.dispatch(connector, new HttpProtocolAccess.Delivery(java.util.UUID.randomUUID().toString(), JsonEnvelope.builder("x").put("marker", "first").build())); - connector.dispatch(new HttpTransportProtocol.Delivery(java.util.UUID.randomUUID().toString(), + HttpTestAccess.dispatch(connector, new HttpProtocolAccess.Delivery(java.util.UUID.randomUUID().toString(), JsonEnvelope.builder("x").put("marker", "second").build())); assertTrue(firstStarted.await(2, TimeUnit.SECONDS)); assertFalse(secondStarted.await(150, TimeUnit.MILLISECONDS)); @@ -1428,10 +1436,10 @@ void backendCallbackQueueBackpressuresWithoutBreakingFifo() throws Exception { HttpClientCredentialStore.HttpClientProfile profile = new HttpClientCredentialStore.HttpClientProfile("lobby-1", java.net.URI.create("https://localhost:8443/"), identity.serverCertificatePin(), identity.caCertificatePin()); CountDownLatch firstStarted = new CountDownLatch(1), releaseFirst = new CountDownLatch(1); - int deliveries = HttpBackendTransportConnector.CALLBACK_QUEUE_CAPACITY + 2; + int deliveries = HttpTestAccess.CALLBACK_QUEUE_CAPACITY + 2; CountDownLatch completed = new CountDownLatch(deliveries), overflowSubmitted = new CountDownLatch(1); java.util.List order = new java.util.concurrent.CopyOnWriteArrayList<>(); - try (HttpBackendTransportConnector connector = new HttpBackendTransportConnector(profile, + try (HttpBackendTransportConnector connector = HttpTestAccess.connector(profile, HttpClientCredentialStore.load(directory.resolve("backpressure-client")), envelope -> { int marker = Integer.parseInt(envelope.getFields().get("marker")); order.add(marker); @@ -1442,12 +1450,12 @@ void backendCallbackQueueBackpressuresWithoutBreakingFifo() throws Exception { } completed.countDown(); })) { - connector.dispatch(delivery(0)); + HttpTestAccess.dispatch(connector, delivery(0)); assertTrue(firstStarted.await(2, TimeUnit.SECONDS)); - for (int marker = 1; marker <= HttpBackendTransportConnector.CALLBACK_QUEUE_CAPACITY; marker++) - connector.dispatch(delivery(marker)); + for (int marker = 1; marker <= HttpTestAccess.CALLBACK_QUEUE_CAPACITY; marker++) + HttpTestAccess.dispatch(connector, delivery(marker)); Thread overflow = new Thread(() -> { - connector.dispatch(delivery(deliveries - 1)); + HttpTestAccess.dispatch(connector, delivery(deliveries - 1)); overflowSubmitted.countDown(); }, "HTTP-overflow-submitter"); overflow.start(); @@ -1472,19 +1480,19 @@ void pausedBackendDefersCallbackAndDurableFenceUntilActivation() throws Exceptio try (HttpBackendTransportConnector connector = new HttpBackendTransportConnector(clientDirectory, ignored -> callback.countDown())) { connector.startPaused(); - connector.dispatch(new HttpTransportProtocol.Delivery(id, JsonEnvelope.builder("vote").build())); + HttpTestAccess.dispatch(connector, new HttpProtocolAccess.Delivery(id, JsonEnvelope.builder("vote").build())); assertFalse(callback.await(150, TimeUnit.MILLISECONDS)); var inboundField = HttpBackendTransportConnector.class.getDeclaredField("inboundDeliveries"); inboundField.setAccessible(true); - HttpInboundDeliveryStore store = (HttpInboundDeliveryStore) inboundField.get(connector); + HttpInboundStoreAccess store = new HttpInboundStoreAccess(inboundField.get(connector)); assertEquals(null, store.state(id), "publication staging must not create a durable replay fence"); connector.activateIncoming(); assertTrue(callback.await(2, TimeUnit.SECONDS)); long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2); - while (store.state(id) != HttpInboundDeliveryStore.State.COMPLETED && System.nanoTime() < deadline) + while (store.state(id) != HttpInboundStoreAccess.State.COMPLETED && System.nanoTime() < deadline) Thread.sleep(5); - assertEquals(HttpInboundDeliveryStore.State.COMPLETED, store.state(id)); + assertEquals(HttpInboundStoreAccess.State.COMPLETED, store.state(id)); } } @@ -1500,7 +1508,7 @@ void restartingPausedBackendWithStartReleasesWaitingCallbacks() throws Exception try (HttpBackendTransportConnector connector = new HttpBackendTransportConnector(clientDirectory, ignored -> callback.countDown())) { connector.startPaused(); - connector.dispatch(new HttpTransportProtocol.Delivery(java.util.UUID.randomUUID().toString(), + HttpTestAccess.dispatch(connector, new HttpProtocolAccess.Delivery(java.util.UUID.randomUUID().toString(), JsonEnvelope.builder("vote").build())); assertFalse(callback.await(150, TimeUnit.MILLISECONDS)); @@ -1521,20 +1529,20 @@ void closingPausedBackendLeavesDeliveryRecoverable() throws Exception { HttpClientCredentialStore.saveEnrolled(clientDirectory, code, issued); AtomicInteger callbacks = new AtomicInteger(); String id = java.util.UUID.randomUUID().toString(); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(id, JsonEnvelope.builder("vote").build()); + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(id, JsonEnvelope.builder("vote").build()); HttpBackendTransportConnector staged = new HttpBackendTransportConnector(clientDirectory, ignored -> callbacks.incrementAndGet()); staged.startPaused(); - staged.dispatch(delivery); + HttpTestAccess.dispatch(staged, delivery); Thread.sleep(150); staged.close(); assertEquals(0, callbacks.get()); try (HttpBackendTransportConnector restarted = new HttpBackendTransportConnector(clientDirectory, ignored -> callbacks.incrementAndGet())) { - assertEquals(java.util.List.of(delivery), restarted.accept(java.util.List.of(delivery)), + assertEquals(java.util.List.of(delivery), HttpTestAccess.accept(restarted, java.util.List.of(delivery)), "abandoned staging must leave the proxy delivery replayable"); - assertTrue(restarted.drainAcknowledgements().isEmpty()); + assertTrue(HttpTestAccess.drainAcknowledgements(restarted).isEmpty()); } } @@ -1548,19 +1556,19 @@ void durableBackendFencePreventsCallbackReplayAfterRestartBeforeAck() throws Exc HttpClientCredentialStore.saveEnrolled(clientDirectory, code, issued); java.util.concurrent.atomic.AtomicInteger callbacks = new java.util.concurrent.atomic.AtomicInteger(); String id = java.util.UUID.randomUUID().toString(); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(id, JsonEnvelope.builder("vote").build()); + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(id, JsonEnvelope.builder("vote").build()); try (HttpBackendTransportConnector first = new HttpBackendTransportConnector(clientDirectory, ignored -> callbacks.incrementAndGet())) { - first.dispatch(delivery); + HttpTestAccess.dispatch(first, delivery); long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2); while (callbacks.get() != 1 && System.nanoTime() < deadline) Thread.sleep(5); assertEquals(1, callbacks.get()); } try (HttpBackendTransportConnector restarted = new HttpBackendTransportConnector(clientDirectory, ignored -> callbacks.incrementAndGet())) { - assertEquals(java.util.List.of(id), restarted.drainAcknowledgements(), + assertEquals(java.util.List.of(id), HttpTestAccess.drainAcknowledgements(restarted), "restart must retain and acknowledge the pre-callback delivery fence"); - assertTrue(restarted.accept(java.util.List.of(delivery)).isEmpty()); + assertTrue(HttpTestAccess.accept(restarted, java.util.List.of(delivery)).isEmpty()); assertEquals(1, callbacks.get(), "a durable proxy replay must not award twice"); } } @@ -1575,19 +1583,19 @@ void failedBackendCallbackRemainsUnacknowledgedAndIsNotReplayed() throws Excepti HttpClientCredentialStore.saveEnrolled(clientDirectory, code, issued); java.util.concurrent.atomic.AtomicInteger attempts = new java.util.concurrent.atomic.AtomicInteger(); CountDownLatch failed = new CountDownLatch(1); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(java.util.UUID.randomUUID().toString(), + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(java.util.UUID.randomUUID().toString(), JsonEnvelope.builder("vote").build()); try (HttpBackendTransportConnector first = new HttpBackendTransportConnector(clientDirectory, ignored -> { attempts.incrementAndGet(); failed.countDown(); throw new IllegalStateException("retry"); })) { - first.dispatch(delivery); + HttpTestAccess.dispatch(first, delivery); assertTrue(failed.await(2, TimeUnit.SECONDS)); - assertTrue(first.drainAcknowledgements().isEmpty()); + assertTrue(HttpTestAccess.drainAcknowledgements(first).isEmpty()); } try (HttpBackendTransportConnector restarted = new HttpBackendTransportConnector(clientDirectory, ignored -> attempts.incrementAndGet())) { - assertTrue(restarted.drainAcknowledgements().isEmpty()); - java.util.List accepted = restarted.accept(java.util.List.of(delivery)); + assertTrue(HttpTestAccess.drainAcknowledgements(restarted).isEmpty()); + java.util.List accepted = HttpTestAccess.accept(restarted, java.util.List.of(delivery)); assertTrue(accepted.isEmpty(), "an ambiguous callback must not be awarded twice"); assertEquals(1, attempts.get()); } @@ -1602,21 +1610,21 @@ void reservedButNotStartedDeliveryResumesAfterRestart() throws Exception { Path clientDirectory = directory.resolve("reserved-client"); HttpClientCredentialStore.saveEnrolled(clientDirectory, code, issued); String id = java.util.UUID.randomUUID().toString(); - HttpInboundDeliveryStore reservedStore = new HttpInboundDeliveryStore(clientDirectory); + HttpInboundStoreAccess reservedStore = new HttpInboundStoreAccess(clientDirectory); reservedStore.reserve(id); reservedStore.seal(); CountDownLatch completed = new CountDownLatch(1); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(id, JsonEnvelope.builder("vote").build()); + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(id, JsonEnvelope.builder("vote").build()); try (HttpBackendTransportConnector restarted = new HttpBackendTransportConnector(clientDirectory, ignored -> completed.countDown())) { - assertTrue(restarted.drainAcknowledgements().isEmpty(), "a reservation alone must never be acknowledged"); - java.util.List accepted = restarted.accept(java.util.List.of(delivery)); + assertTrue(HttpTestAccess.drainAcknowledgements(restarted).isEmpty(), "a reservation alone must never be acknowledged"); + java.util.List accepted = HttpTestAccess.accept(restarted, java.util.List.of(delivery)); assertEquals(1, accepted.size()); - restarted.dispatch(accepted.get(0)); + HttpTestAccess.dispatch(restarted, accepted.get(0)); assertTrue(completed.await(2, TimeUnit.SECONDS)); long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2); java.util.List acknowledgements = java.util.List.of(); while (acknowledgements.isEmpty() && System.nanoTime() < deadline) { - acknowledgements = restarted.drainAcknowledgements(); + acknowledgements = HttpTestAccess.drainAcknowledgements(restarted); if (acknowledgements.isEmpty()) Thread.sleep(5); } assertEquals(java.util.List.of(id), acknowledgements); @@ -1634,17 +1642,17 @@ void interruptedStateRenameRetainsTheFurthestSafeState() throws Exception { Files.writeString(states.resolve(id + ".running"), id); Files.writeString(states.resolve(completedId + ".running"), completedId); Files.writeString(states.resolve(completedId + ".completed"), completedId); - HttpInboundDeliveryStore store = new HttpInboundDeliveryStore(clientDirectory); - assertEquals(HttpInboundDeliveryStore.State.RUNNING, store.state(id)); - assertEquals(HttpInboundDeliveryStore.State.COMPLETED, store.state(completedId)); + HttpInboundStoreAccess store = new HttpInboundStoreAccess(clientDirectory); + assertEquals(HttpInboundStoreAccess.State.RUNNING, store.state(id)); + assertEquals(HttpInboundStoreAccess.State.COMPLETED, store.state(completedId)); assertFalse(Files.exists(states.resolve(id + ".reserved"))); assertTrue(Files.exists(states.resolve(id + ".running"))); assertFalse(Files.exists(states.resolve(completedId + ".running"))); assertTrue(Files.exists(states.resolve(completedId + ".completed"))); } - private static HttpTransportProtocol.Delivery delivery(int marker) { - return new HttpTransportProtocol.Delivery(java.util.UUID.randomUUID().toString(), + private static HttpProtocolAccess.Delivery delivery(int marker) { + return new HttpProtocolAccess.Delivery(java.util.UUID.randomUUID().toString(), JsonEnvelope.builder("x").put("marker", marker).build()); } @@ -1654,7 +1662,7 @@ void proxyCallbacksAreSerializedInDeliveryOrder() throws Exception { HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, directory.resolve("ordered-authority")); CountDownLatch firstStarted = new CountDownLatch(1), releaseFirst = new CountDownLatch(1), secondStarted = new CountDownLatch(1); java.util.List order = new java.util.concurrent.CopyOnWriteArrayList<>(); - try (HttpProxyTransportServer server = new HttpProxyTransportServer(new InetSocketAddress("localhost", 0), identity, + try (HttpProxyTransportServer server = HttpTestAccess.server(new InetSocketAddress("localhost", 0), identity, authority, received -> { String marker = String.valueOf(received.envelope().getFields().get("marker")); order.add(marker); @@ -1668,7 +1676,7 @@ void proxyCallbacksAreSerializedInDeliveryOrder() throws Exception { HttpConnectionCode code = authority.createConnectionCode("lobby-1", server.endpoint("localhost"), Duration.ofMinutes(5)); HttpClientCredentialStore.ClientCredential credential = HttpBackendTransportConnector.enroll(code, "lobby-1", directory.resolve("ordered-proxy-client")); - try (HttpBackendTransportConnector connector = new HttpBackendTransportConnector(code, "lobby-1", credential, + try (HttpBackendTransportConnector connector = HttpTestAccess.connector(code, "lobby-1", credential, ignored -> { })) { connector.start(); assertTrue(connector.send(JsonEnvelope.builder("x").put("marker", "first").build())); @@ -1689,17 +1697,17 @@ void backendDedupWindowContinuesAfterCapacity() throws Exception { HttpClientCredentialStore.save(directory.resolve("client"), issued); HttpClientCredentialStore.HttpClientProfile profile = new HttpClientCredentialStore.HttpClientProfile("lobby-1", java.net.URI.create("https://localhost:8443/"), identity.serverCertificatePin(), identity.caCertificatePin()); - try (HttpBackendTransportConnector connector = new HttpBackendTransportConnector(profile, + try (HttpBackendTransportConnector connector = HttpTestAccess.connector(profile, HttpClientCredentialStore.load(directory.resolve("client")), envelope -> { })) { - for (int index = 0; index < HttpTransportProtocol.MAX_QUEUE; index++) { + for (int index = 0; index < HttpProtocolAccess.MAX_QUEUE; index++) { String id = java.util.UUID.randomUUID().toString(); - HttpTransportProtocol.Delivery delivery = new HttpTransportProtocol.Delivery(id, JsonEnvelope.builder("x").build()); - assertEquals(1, connector.accept(java.util.List.of(delivery)).size()); - connector.completeIncoming(id, true); + HttpProtocolAccess.Delivery delivery = new HttpProtocolAccess.Delivery(id, JsonEnvelope.builder("x").build()); + assertEquals(1, HttpTestAccess.accept(connector, java.util.List.of(delivery)).size()); + HttpTestAccess.completeIncoming(connector, id, true); } - HttpTransportProtocol.Delivery next = new HttpTransportProtocol.Delivery(java.util.UUID.randomUUID().toString(), + HttpProtocolAccess.Delivery next = new HttpProtocolAccess.Delivery(java.util.UUID.randomUUID().toString(), JsonEnvelope.builder("x").build()); - assertEquals(1, connector.accept(java.util.List.of(next)).size()); + assertEquals(1, HttpTestAccess.accept(connector, java.util.List.of(next)).size()); } } } diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpTransportSecurityTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpTransportSecurityTest.java similarity index 88% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpTransportSecurityTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpTransportSecurityTest.java index b1ee09b8..1b78dbd8 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/HttpTransportSecurityTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/HttpTransportSecurityTest.java @@ -1,4 +1,11 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpBackendTransportConnector; +import com.bencodez.simpleapi.servercomm.http.HttpClientCredentialStore; +import com.bencodez.simpleapi.servercomm.http.HttpConnectionCode; +import com.bencodez.simpleapi.servercomm.http.HttpEnrollmentAuthority; +import com.bencodez.simpleapi.servercomm.http.HttpPinnedTls; +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -28,25 +35,25 @@ class HttpTransportSecurityTest { @Test void failedRenewalRetriesBeforeActiveCertificateExpires() { assertEquals(Duration.ofMinutes(5), - HttpBackendTransportConnector.renewalRetryDelay(Duration.ofHours(6))); + HttpTestAccess.renewalRetryDelay(Duration.ofHours(6))); assertEquals(Duration.ofMinutes(1), - HttpBackendTransportConnector.renewalRetryDelay(Duration.ofMinutes(4))); - assertTrue(HttpBackendTransportConnector.renewalRetryDelay(Duration.ofSeconds(3)) + HttpTestAccess.renewalRetryDelay(Duration.ofMinutes(4))); + assertTrue(HttpTestAccess.renewalRetryDelay(Duration.ofSeconds(3)) .compareTo(Duration.ofSeconds(3)) < 0); } @Test void requestTimeoutConversionPreservesSubMillisecondDurations() { - assertEquals(1L, HttpBackendTransportConnector.timeoutNanos(Duration.ofNanos(1L))); - assertEquals(999_999L, HttpBackendTransportConnector.timeoutNanos(Duration.ofNanos(999_999L))); - assertEquals(Long.MAX_VALUE, HttpBackendTransportConnector.timeoutNanos(Duration.ofSeconds(Long.MAX_VALUE))); + assertEquals(1L, HttpTestAccess.timeoutNanos(Duration.ofNanos(1L))); + assertEquals(999_999L, HttpTestAccess.timeoutNanos(Duration.ofNanos(999_999L))); + assertEquals(Long.MAX_VALUE, HttpTestAccess.timeoutNanos(Duration.ofSeconds(Long.MAX_VALUE))); } @Test void renewalDeadlineHandlesNegativeNanoTimeOrigins() { - assertEquals(-9L, HttpBackendTransportConnector.renewalDeadline(-10L, Duration.ofNanos(1L))); - assertEquals(Long.MIN_VALUE + 1L, HttpBackendTransportConnector.renewalDeadline(Long.MIN_VALUE, Duration.ofNanos(1L))); - assertEquals(Long.MIN_VALUE, HttpBackendTransportConnector.renewalDeadline(Long.MAX_VALUE, Duration.ofNanos(1L))); + assertEquals(-9L, HttpTestAccess.renewalDeadline(-10L, Duration.ofNanos(1L))); + assertEquals(Long.MIN_VALUE + 1L, HttpTestAccess.renewalDeadline(Long.MIN_VALUE, Duration.ofNanos(1L))); + assertEquals(Long.MIN_VALUE, HttpTestAccess.renewalDeadline(Long.MAX_VALUE, Duration.ofNanos(1L))); } @Test @@ -70,9 +77,9 @@ void tlsIdentityRejectsInvalidIpAndNonAsciiAlternativeNames() { @Test void tlsIdentityAcceptsIpv4MappedIpv6AlternativeNames() { - assertTrue(JdkX509CertificateGenerator.isIpLiteral("::ffff:192.0.2.1")); - assertTrue(JdkX509CertificateGenerator.isIpLiteral("::ffff:c000:201")); - assertTrue(JdkX509CertificateGenerator.isIpLiteral("0:0:0:0:0:ffff:c000:201")); + assertTrue(HttpTestAccess.isIpLiteral("::ffff:192.0.2.1")); + assertTrue(HttpTestAccess.isIpLiteral("::ffff:c000:201")); + assertTrue(HttpTestAccess.isIpLiteral("0:0:0:0:0:ffff:c000:201")); assertDoesNotThrow(() -> HttpTlsIdentity.loadOrCreate(directory.resolve("mapped-ipv6-dotted"), "::ffff:192.0.2.1")); assertDoesNotThrow(() -> HttpTlsIdentity.loadOrCreate(directory.resolve("mapped-ipv6-hex"), @@ -81,17 +88,17 @@ void tlsIdentityAcceptsIpv4MappedIpv6AlternativeNames() { @Test void backendResponseReaderRejectsBodiesBeyondTheWireLimit() throws Exception { - byte[] maximum = new byte[HttpTransportProtocol.MAX_BODY_BYTES]; - assertEquals(maximum.length, HttpBackendTransportConnector.readLimited( + byte[] maximum = new byte[HttpProtocolAccess.MAX_BODY_BYTES]; + assertEquals(maximum.length, HttpTestAccess.readLimited( new java.io.ByteArrayInputStream(maximum)).length); - assertThrows(java.io.IOException.class, () -> HttpBackendTransportConnector.readLimited( - new java.io.ByteArrayInputStream(new byte[HttpTransportProtocol.MAX_BODY_BYTES + 1]))); + assertThrows(java.io.IOException.class, () -> HttpTestAccess.readLimited( + new java.io.ByteArrayInputStream(new byte[HttpProtocolAccess.MAX_BODY_BYTES + 1]))); } @Test void connectionCodeRoundTripsAndRejectsAccidentalCorruption() { HttpConnectionCode original = new HttpConnectionCode("lobby.eu", URI.create("https://Proxy.Example.test:8443/http"), pin('a'), pin('b'), - Instant.parse("2030-01-01T00:00:00Z"), HttpTransportSecrets.randomToken()); + Instant.parse("2030-01-01T00:00:00Z"), HttpSecretsAccess.randomToken()); String encoded = original.encode(); HttpConnectionCode parsed = HttpConnectionCode.parse(encoded); assertEquals("lobby.eu", parsed.serverId()); @@ -107,7 +114,7 @@ void connectionCodeRoundTripsAndRejectsAccidentalCorruption() { void connectionCodePreservesEscapedEndpointPaths() { HttpConnectionCode original = new HttpConnectionCode("lobby.eu", URI.create("https://Proxy.Example.test:8443/api%20root/%2F"), pin('a'), pin('b'), - Instant.parse("2030-01-01T00:00:00Z"), HttpTransportSecrets.randomToken()); + Instant.parse("2030-01-01T00:00:00Z"), HttpSecretsAccess.randomToken()); URI expected = URI.create("https://proxy.example.test:8443/api%20root/%2F/"); assertEquals(expected, original.endpoint()); assertEquals(expected, HttpConnectionCode.parse(original.encode()).endpoint()); @@ -119,30 +126,30 @@ void legacyConnectionCodesAndConsumedMarkersRemainCompatible() throws Exception HttpConnectionCode legacy = new HttpConnectionCode("lobby", URI.create("https://proxy.example.test:8443/"), identity.serverCertificatePin(), identity.caCertificatePin(), Instant.now().plusSeconds(60).truncatedTo(java.time.temporal.ChronoUnit.SECONDS), - HttpTransportSecrets.randomToken()); - assertEquals(legacy, HttpConnectionCode.parse(legacy.encodeLegacy())); + HttpSecretsAccess.randomToken()); + assertEquals(legacy, HttpConnectionCode.parse(HttpTestAccess.encodeLegacy(legacy))); Path client = directory.resolve("legacy-code-client"); HttpClientCredentialStore.saveEnrolled(client, legacy, identity.issueClientCertificate("lobby")); Path active = client.resolve("http-transport-client-generations") .resolve(Files.readString(client.resolve("http-transport-client-current"))); Files.writeString(active.resolve("http-transport-connection-code.sha256"), - HttpTransportSecrets.sha256Hex(legacy.encodeLegacy().getBytes(java.nio.charset.StandardCharsets.US_ASCII))); + HttpSecretsAccess.sha256Hex(HttpTestAccess.encodeLegacy(legacy).getBytes(java.nio.charset.StandardCharsets.US_ASCII))); assertTrue(HttpClientCredentialStore.matchesEnrollmentCode(client, - HttpConnectionCode.parse(legacy.encodeLegacy()))); + HttpConnectionCode.parse(HttpTestAccess.encodeLegacy(legacy)))); } @Test void legacyConnectionCodesSupportDottedServerIds() { HttpConnectionCode original = new HttpConnectionCode("lobby.eu", URI.create("https://proxy.example.test:8443/"), - pin('a'), pin('b'), Instant.parse("2030-01-01T00:00:00Z"), HttpTransportSecrets.randomToken()); - assertEquals(original, HttpConnectionCode.parse(original.encodeLegacy())); + pin('a'), pin('b'), Instant.parse("2030-01-01T00:00:00Z"), HttpSecretsAccess.randomToken()); + assertEquals(original, HttpConnectionCode.parse(HttpTestAccess.encodeLegacy(original))); } @Test void expiredCodesAreNotActive() { HttpConnectionCode code = new HttpConnectionCode("lobby", URI.create("https://proxy.example.test/"), pin('a'), pin('b'), - Instant.parse("2029-12-31T23:59:59Z"), HttpTransportSecrets.randomToken()); + Instant.parse("2029-12-31T23:59:59Z"), HttpSecretsAccess.randomToken()); assertFalse(code.isActive(Clock.fixed(Instant.parse("2030-01-01T00:00:00Z"), ZoneOffset.UTC))); assertThrows(IllegalArgumentException.class, () -> code.requireActive(Clock.fixed(Instant.parse("2030-01-01T00:00:00Z"), ZoneOffset.UTC))); } @@ -153,11 +160,11 @@ void inboundDeliveryFenceRejectsCorruptionAndPathReplacement() throws Exception Path corruptFence = corruptCredentials.resolve("http-transport-inbound-deliveries"); Files.createDirectories(corruptFence); Files.writeString(corruptFence.resolve("not-a-delivery.seen"), "not-a-delivery"); - assertThrows(java.io.IOException.class, () -> new HttpInboundDeliveryStore(corruptCredentials)); + assertThrows(java.io.IOException.class, () -> new HttpInboundStoreAccess(corruptCredentials)); Path replacedCredentials = directory.resolve("replaced-client"); Files.createDirectories(replacedCredentials); - HttpInboundDeliveryStore store = new HttpInboundDeliveryStore(replacedCredentials); + HttpInboundStoreAccess store = new HttpInboundStoreAccess(replacedCredentials); Path fence = replacedCredentials.resolve("http-transport-inbound-deliveries"); Path outside = directory.resolve("outside-fence"); Files.createDirectory(outside); @@ -171,12 +178,12 @@ void sealedInboundStoreCannotChangeAfterOwnershipHandoff() throws Exception { Path credentials = directory.resolve("sealed-client"); Files.createDirectories(credentials); String id = java.util.UUID.randomUUID().toString(); - HttpInboundDeliveryStore store = new HttpInboundDeliveryStore(credentials); + HttpInboundStoreAccess store = new HttpInboundStoreAccess(credentials); store.reserve(id); store.markRunning(id); store.seal(); assertThrows(java.io.IOException.class, () -> store.markCompleted(id)); - assertEquals(HttpInboundDeliveryStore.State.RUNNING, new HttpInboundDeliveryStore(credentials).state(id)); + assertEquals(HttpInboundStoreAccess.State.RUNNING, new HttpInboundStoreAccess(credentials).state(id)); } @Test @@ -186,9 +193,9 @@ void identityIsDurableAndPinsRejectTheWrongServer() throws Exception { assertEquals(created.serverCertificatePin(), loaded.serverCertificatePin()); assertEquals(created.caCertificatePin(), loaded.caCertificatePin()); HttpConnectionCode correct = new HttpConnectionCode("lobby", URI.create("https://localhost:8443/"), created.serverCertificatePin(), - created.caCertificatePin(), Instant.now().plusSeconds(60), HttpTransportSecrets.randomToken()); + created.caCertificatePin(), Instant.now().plusSeconds(60), HttpSecretsAccess.randomToken()); HttpConnectionCode incorrect = new HttpConnectionCode("lobby", URI.create("https://localhost:8443/"), pin('0'), created.caCertificatePin(), - Instant.now().plusSeconds(60), HttpTransportSecrets.randomToken()); + Instant.now().plusSeconds(60), HttpSecretsAccess.randomToken()); assertTrue(HttpPinnedTls.matchesServerPin(correct, created.serverCertificate())); assertFalse(HttpPinnedTls.matchesServerPin(incorrect, created.serverCertificate())); assertTrue(Files.exists(directory.resolve("http-transport-ca.p12"))); @@ -299,7 +306,7 @@ void incompleteEstablishedTlsIdentityFailsClosed() throws Exception { void enrollmentIsSingleUseBoundToServerAndRevocable() throws Exception { HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory, "localhost"); Clock clock = Clock.fixed(Instant.parse("2030-01-01T00:00:00Z"), ZoneOffset.UTC); - HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, clock); + HttpEnrollmentAuthority authority = HttpTestAccess.authority(identity, clock); HttpConnectionCode wrongTargetCode = authority.createConnectionCode("lobby-1", URI.create("https://localhost:8443/"), Duration.ofMinutes(5)); assertThrows(IllegalArgumentException.class, () -> authority.enroll("attacker", wrongTargetCode.enrollmentToken())); assertTrue(authority.authenticate("lobby-1", authority.enroll("lobby-1", wrongTargetCode.enrollmentToken()).certificate()), @@ -319,7 +326,7 @@ void enrollmentIsSingleUseBoundToServerAndRevocable() throws Exception { assertFalse(authority.authenticate("lobby-1", issued.certificate())); HttpClientCredentialStore.saveEnrolled(directory.resolve("client"), code, issued); HttpClientCredentialStore.ClientCredential restored = HttpClientCredentialStore.load(directory.resolve("client")); - assertEquals(HttpTransportSecrets.certificatePin(issued.certificate()), HttpTransportSecrets.certificatePin(restored.certificate())); + assertEquals(HttpSecretsAccess.certificatePin(issued.certificate()), HttpSecretsAccess.certificatePin(restored.certificate())); HttpClientCredentialStore.HttpClientProfile profile = HttpClientCredentialStore.loadProfile(directory.resolve("client")); assertEquals("lobby-1", profile.serverId()); assertEquals(code.endpoint(), profile.endpoint()); @@ -341,7 +348,7 @@ void enrollmentIsSingleUseBoundToServerAndRevocable() throws Exception { @Test void revocationInvalidatesEveryPendingCodeForTheBackend() throws Exception { HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("revoke-pending"), "localhost"); - HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, + HttpEnrollmentAuthority authority = HttpTestAccess.authority(identity, Clock.fixed(Instant.parse("2030-01-01T00:00:00Z"), ZoneOffset.UTC)); URI endpoint = URI.create("https://localhost:8443/"); HttpConnectionCode beforeEnrollment = authority.createConnectionCode("lobby-1", endpoint, Duration.ofMinutes(5)); @@ -495,7 +502,7 @@ void enrollmentPublicationFailureDisablesAuthenticationUntilCodePersistenceRecov @Override public Clock withZone(java.time.ZoneId zone) { return this; } @Override public Instant instant() { return now.get(); } }; - HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, clock, + HttpEnrollmentAuthority authority = HttpTestAccess.authority(identity, clock, stateDirectory.resolve("http-transport-clients.properties")); URI endpoint = URI.create("https://localhost:8443/"); HttpConnectionCode unrelatedCode = authority.createConnectionCode("unrelated", endpoint, Duration.ofMinutes(5)); @@ -665,7 +672,7 @@ void failedRenewalPersistenceRestoresTheActiveBindingAndCanRetry() throws Except @Override public Clock withZone(java.time.ZoneId zone) { return this; } @Override public Instant instant() { return now.get(); } }; - HttpEnrollmentAuthority authority = new HttpEnrollmentAuthority(identity, clock, + HttpEnrollmentAuthority authority = HttpTestAccess.authority(identity, clock, stateDirectory.resolve("http-transport-clients.properties")); HttpConnectionCode code = authority.createConnectionCode("lobby-1", URI.create("https://localhost:8443/"), Duration.ofMinutes(5)); @@ -688,44 +695,44 @@ void failedRenewalPersistenceRestoresTheActiveBindingAndCanRetry() throws Except @Test void serverLeafRotatesInsideRenewalWindowAndPreservesAuthority() throws Exception { Instant now = Instant.now(); - HttpTlsIdentity original = HttpTlsIdentity.loadOrCreate(directory, "localhost", + HttpTlsIdentity original = HttpTestAccess.identity(directory, "localhost", Clock.fixed(now.minus(Duration.ofDays(340)), ZoneOffset.UTC)); - String originalPin = HttpTransportSecrets.certificatePin(original.serverCertificate()); - HttpTlsIdentity renewed = HttpTlsIdentity.loadOrCreate(directory, "localhost", Clock.fixed(now, ZoneOffset.UTC)); + String originalPin = HttpSecretsAccess.certificatePin(original.serverCertificate()); + HttpTlsIdentity renewed = HttpTestAccess.identity(directory, "localhost", Clock.fixed(now, ZoneOffset.UTC)); assertNotEquals(originalPin, renewed.serverCertificatePin()); assertEquals(original.caCertificatePin(), renewed.caCertificatePin()); - assertFalse(HttpTlsIdentity.needsRenewal(renewed.serverCertificate(), Clock.fixed(now, ZoneOffset.UTC))); + assertFalse(HttpTestAccess.needsRenewal(renewed.serverCertificate(), Clock.fixed(now, ZoneOffset.UTC))); } @Test void runningPrivateCaRollsOverBeforeExpiryWithoutStrandingExistingClients() throws Exception { Instant now = Instant.now(); Clock originalClock = Clock.fixed(now.minus(Duration.ofDays(9 * 365L + 30L)), ZoneOffset.UTC); - HttpTlsIdentity original = HttpTlsIdentity.loadOrCreate(directory, "localhost", originalClock); + HttpTlsIdentity original = HttpTestAccess.identity(directory, "localhost", originalClock); X509Certificate originalCa = original.caCertificate(); - HttpTlsIdentity.IssuedClientCertificate existingClient = original.issueClientCertificate("lobby-1", now); + HttpTlsIdentity.IssuedClientCertificate existingClient = HttpTestAccess.issueClientCertificate(original, "lobby-1", now); Path client = directory.resolve("client"); HttpConnectionCode oldCode = new HttpConnectionCode("lobby-1", URI.create("https://localhost:8443/"), - HttpTransportSecrets.certificatePin(original.serverCertificate()), HttpTransportSecrets.certificatePin(originalCa), + HttpSecretsAccess.certificatePin(original.serverCertificate()), HttpSecretsAccess.certificatePin(originalCa), now.plusSeconds(60), "A".repeat(43)); HttpClientCredentialStore.saveEnrolled(client, oldCode, existingClient); String renewedPin = original.caCertificatePin(); - assertNotEquals(HttpTransportSecrets.certificatePin(originalCa), renewedPin); + assertNotEquals(HttpSecretsAccess.certificatePin(originalCa), renewedPin); assertEquals(originalCa.getPublicKey(), original.caCertificate().getPublicKey(), "certificate rollover keeps the private authority key so old and new trust anchors overlap"); - assertFalse(HttpTlsIdentity.needsCaRenewal(original.caCertificate(), Clock.fixed(now, ZoneOffset.UTC))); + assertFalse(HttpTestAccess.needsCaRenewal(original.caCertificate(), Clock.fixed(now, ZoneOffset.UTC))); assertTrue(original.validClientCertificate("lobby-1", existingClient.certificate())); - X509TrustManager oldClientTrust = Arrays.stream(HttpTlsIdentity.trustManagers(originalCa)) + X509TrustManager oldClientTrust = Arrays.stream(HttpTestAccess.trustManagers(originalCa)) .filter(X509TrustManager.class::isInstance).map(X509TrustManager.class::cast).findFirst().orElseThrow(); assertDoesNotThrow(() -> oldClientTrust.checkServerTrusted( new X509Certificate[] { original.serverCertificate(), original.caCertificate() }, "ECDHE_ECDSA")); - HttpClientCredentialStore.StagedCredential staged = HttpClientCredentialStore.stageReplacement(client, - original.issueClientCertificate("lobby-1", now)); - assertEquals(HttpTransportSecrets.certificatePin(originalCa), HttpClientCredentialStore.loadProfile(client).caCertificatePin()); + HttpTestAccess.StagedCredential staged = HttpTestAccess.stageReplacement(client, + HttpTestAccess.issueClientCertificate(original, "lobby-1", now)); + assertEquals(HttpSecretsAccess.certificatePin(originalCa), HttpClientCredentialStore.loadProfile(client).caCertificatePin()); assertEquals(renewedPin, staged.profile().caCertificatePin()); - HttpClientCredentialStore.activateReplacement(client, staged); + HttpTestAccess.activateReplacement(client, staged); assertEquals(renewedPin, HttpClientCredentialStore.loadProfile(client).caCertificatePin()); assertEquals(renewedPin, HttpTlsIdentity.loadOrCreate(directory, "localhost").caCertificatePin(), "live CA rollover must survive restart"); @@ -734,19 +741,19 @@ void runningPrivateCaRollsOverBeforeExpiryWithoutStrandingExistingClients() thro @Test void activeTlsContextRotatesServerLeafInsideRenewalWindow() throws Exception { Instant now = Instant.now(); - HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory, "localhost", + HttpTlsIdentity identity = HttpTestAccess.identity(directory, "localhost", Clock.fixed(now.minus(Duration.ofDays(340)), ZoneOffset.UTC)); - String expiringPin = HttpTransportSecrets.certificatePin(identity.serverCertificate()); + String expiringPin = HttpSecretsAccess.certificatePin(identity.serverCertificate()); identity.serverContext(); assertNotEquals(expiringPin, identity.serverCertificatePin()); - assertFalse(HttpTlsIdentity.needsRenewal(identity.serverCertificate(), Clock.systemUTC())); + assertFalse(HttpTestAccess.needsRenewal(identity.serverCertificate(), Clock.systemUTC())); } @Test void serverTlsUsesPrivateCaTrustAndRejectsForeignClients() throws Exception { HttpTlsIdentity identity = HttpTlsIdentity.loadOrCreate(directory.resolve("proxy"), "localhost"); HttpTlsIdentity foreign = HttpTlsIdentity.loadOrCreate(directory.resolve("foreign"), "localhost"); - X509TrustManager trust = Arrays.stream(HttpTlsIdentity.trustManagers(identity.caCertificate())) + X509TrustManager trust = Arrays.stream(HttpTestAccess.trustManagers(identity.caCertificate())) .filter(X509TrustManager.class::isInstance).map(X509TrustManager.class::cast).findFirst().orElseThrow(); HttpTlsIdentity.IssuedClientCertificate accepted = identity.issueClientCertificate("lobby-1"); HttpTlsIdentity.IssuedClientCertificate rejected = foreign.issueClientCertificate("lobby-1"); @@ -764,18 +771,18 @@ void stagedCredentialDoesNotReplaceActiveGenerationUntilAtomicActivation() throw HttpConnectionCode code = new HttpConnectionCode("lobby-1", URI.create("https://localhost:8443/"), identity.serverCertificatePin(), identity.caCertificatePin(), Instant.now().plusSeconds(60), "A".repeat(43)); HttpClientCredentialStore.saveEnrolled(client, code, original); - String originalPin = HttpTransportSecrets.certificatePin(HttpClientCredentialStore.load(client).certificate()); + String originalPin = HttpSecretsAccess.certificatePin(HttpClientCredentialStore.load(client).certificate()); HttpTlsIdentity.IssuedClientCertificate replacement = identity.issueClientCertificate("lobby-1"); - HttpClientCredentialStore.StagedCredential staged = HttpClientCredentialStore.stageReplacement(client, replacement); - assertEquals(originalPin, HttpTransportSecrets.certificatePin(HttpClientCredentialStore.load(client).certificate())); - HttpClientCredentialStore.activateReplacement(client, staged); - assertNotEquals(originalPin, HttpTransportSecrets.certificatePin(HttpClientCredentialStore.load(client).certificate())); + HttpTestAccess.StagedCredential staged = HttpTestAccess.stageReplacement(client, replacement); + assertEquals(originalPin, HttpSecretsAccess.certificatePin(HttpClientCredentialStore.load(client).certificate())); + HttpTestAccess.activateReplacement(client, staged); + assertNotEquals(originalPin, HttpSecretsAccess.certificatePin(HttpClientCredentialStore.load(client).certificate())); assertTrue(HttpClientCredentialStore.matchesEnrollmentCode(client, code), "automatic certificate renewal must retain the consumed-code marker"); HttpTlsIdentity.IssuedClientCertificate manuallyReenrolled = identity.issueClientCertificate("lobby-1"); HttpClientCredentialStore.saveEnrolled(client, code, manuallyReenrolled); - assertEquals(HttpTransportSecrets.certificatePin(manuallyReenrolled.certificate()), - HttpTransportSecrets.certificatePin(HttpClientCredentialStore.loadEnrolled(client).credential().certificate())); + assertEquals(HttpSecretsAccess.certificatePin(manuallyReenrolled.certificate()), + HttpSecretsAccess.certificatePin(HttpClientCredentialStore.loadEnrolled(client).credential().certificate())); } @Test @@ -786,9 +793,9 @@ void credentialReplacementReclaimsSupersededGenerations() throws Exception { identity.serverCertificatePin(), identity.caCertificatePin(), Instant.now().plusSeconds(60), "R".repeat(43)); HttpClientCredentialStore.saveEnrolled(client, code, identity.issueClientCertificate("lobby-1")); for (int i = 0; i < 5; i++) { - HttpClientCredentialStore.StagedCredential staged = HttpClientCredentialStore.stageReplacement(client, + HttpTestAccess.StagedCredential staged = HttpTestAccess.stageReplacement(client, identity.issueClientCertificate("lobby-1")); - HttpClientCredentialStore.activateReplacement(client, staged); + HttpTestAccess.activateReplacement(client, staged); } Path generations = client.resolve("http-transport-client-generations"); try (var entries = Files.list(generations)) { @@ -812,11 +819,11 @@ void activationRemainsUsableWhenStaleGenerationCleanupFails() throws Exception { .resolve("11111111-1111-1111-1111-111111111111"); Files.createDirectories(reclaimable); Files.writeString(reclaimable.resolve("orphan"), "orphan"); - HttpClientCredentialStore.StagedCredential staged = HttpClientCredentialStore.stageReplacement(client, + HttpTestAccess.StagedCredential staged = HttpTestAccess.stageReplacement(client, identity.issueClientCertificate("lobby-1")); - assertDoesNotThrow(() -> HttpClientCredentialStore.activateReplacement(client, staged)); - assertEquals(HttpTransportSecrets.certificatePin(staged.credential().certificate()), - HttpTransportSecrets.certificatePin(HttpClientCredentialStore.loadEnrolled(client).credential().certificate())); + assertDoesNotThrow(() -> HttpTestAccess.activateReplacement(client, staged)); + assertEquals(HttpSecretsAccess.certificatePin(staged.credential().certificate()), + HttpSecretsAccess.certificatePin(HttpClientCredentialStore.loadEnrolled(client).credential().certificate())); assertFalse(Files.exists(reclaimable), "cleanup must continue past an unsafe stale generation"); assertTrue(Files.exists(stale), "unsafe stale generations must be retained for a later retry"); } @@ -831,7 +838,7 @@ void enrollmentLockSerializesCredentialMutations() throws Exception { try { for (int i = 0; i < 2; i++) executor.submit(() -> { try { - HttpClientCredentialStore.withEnrollmentLock(client, () -> { + HttpTestAccess.withEnrollmentLock(client, () -> { started.countDown(); int current = active.incrementAndGet(); maximum.accumulateAndGet(current, Math::max); @@ -889,8 +896,8 @@ void rollbackRetainsNewerCredentialForTheSameEndpoint() throws Exception { HttpClientCredentialStore.saveEnrolled(client, replacementCode, renewed); HttpClientCredentialStore.restoreActiveGenerationAfterReplacement(client, previous); - assertEquals(HttpTransportSecrets.certificatePin(renewed.certificate()), - HttpTransportSecrets.certificatePin(HttpClientCredentialStore.load(client).certificate()), + assertEquals(HttpSecretsAccess.certificatePin(renewed.certificate()), + HttpSecretsAccess.certificatePin(HttpClientCredentialStore.load(client).certificate()), "rollback must not reactivate a same-endpoint certificate that renewal may have revoked"); assertTrue(HttpClientCredentialStore.matchesEnrollmentCode(client, code), "the retained credential must recognize the connection code restored in YAML"); diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/PackagedTlsSmoke.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/PackagedTlsSmoke.java similarity index 88% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/PackagedTlsSmoke.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/PackagedTlsSmoke.java index ea1ff6bc..c8a6ce18 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/http/PackagedTlsSmoke.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/http/PackagedTlsSmoke.java @@ -1,4 +1,6 @@ -package com.bencodez.simpleapi.servercomm.http; +package com.bencodez.simpleapi.tests.servercomm.http; + +import com.bencodez.simpleapi.servercomm.http.HttpTlsIdentity; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -54,11 +56,11 @@ public static void main(String[] args) throws Exception { HttpTlsIdentity reloaded = HttpTlsIdentity.loadOrCreate(state.resolve("trusted"), "127.0.0.1"); require(Arrays.equals(server.getEncoded(), reloaded.serverCertificate().getEncoded()), "Reload replaced the identity"); - HttpTlsIdentity renewed = HttpTlsIdentity.loadOrCreate(state.resolve("trusted"), "127.0.0.1", + HttpTlsIdentity renewed = loadIdentity(state.resolve("trusted"), "127.0.0.1", Clock.offset(Clock.systemUTC(), Duration.ofDays(340))); renewed.serverCertificate().verify(ca.getPublicKey()); require(renewed.serverCertificate().getNotAfter().after(server.getNotAfter()), "Server renewal failed"); - HttpTlsIdentity renewedCa = HttpTlsIdentity.loadOrCreate(state.resolve("trusted"), "127.0.0.1", + HttpTlsIdentity renewedCa = loadIdentity(state.resolve("trusted"), "127.0.0.1", Clock.offset(Clock.systemUTC(), Duration.ofDays(3400))); renewedCa.caCertificate().verify(ca.getPublicKey()); renewedCa.serverCertificate().verify(renewedCa.caCertificate().getPublicKey()); @@ -132,6 +134,17 @@ private static void exchange(SSLContext serverContext, SSLContext clientContext, } } + private static HttpTlsIdentity loadIdentity(Path directory, String host, Clock clock) throws Exception { + var method = HttpTlsIdentity.class.getDeclaredMethod("loadOrCreate", Path.class, String.class, Clock.class); + method.setAccessible(true); + try { return (HttpTlsIdentity) method.invoke(null, directory, host, clock); } + catch (java.lang.reflect.InvocationTargetException failure) { + if (failure.getCause() instanceof Exception exception) throw exception; + if (failure.getCause() instanceof Error error) throw error; + throw new AssertionError(failure.getCause()); + } + } + private static void requireFromJar(Class type, Path full) throws Exception { Path source = Path.of(type.getProtectionDomain().getCodeSource().getLocation().toURI()); require(Files.isSameFile(source, full), "Loaded outside packaged artifact: " + type.getName()); diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/mqtt/MqttServerCommLifecycleTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/mqtt/MqttServerCommLifecycleTest.java similarity index 95% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/mqtt/MqttServerCommLifecycleTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/mqtt/MqttServerCommLifecycleTest.java index 4eb6ea25..7f2081b3 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/mqtt/MqttServerCommLifecycleTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/mqtt/MqttServerCommLifecycleTest.java @@ -1,4 +1,6 @@ -package com.bencodez.simpleapi.servercomm.mqtt; +package com.bencodez.simpleapi.tests.servercomm.mqtt; + +import com.bencodez.simpleapi.servercomm.mqtt.MqttServerComm; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; @@ -37,7 +39,8 @@ private static final class Fixture implements AutoCloseable { when(client.disconnect()).thenAnswer(call -> { connected.set(false); return token; }); when(client.subscribe(anyString(), anyInt(), any(IMqttMessageListener.class))).thenReturn(token); when(client.subscribe(anyString(), anyInt(), isNull(), any(IMqttActionListener.class), any(IMqttMessageListener.class))).thenReturn(token); - comm = new MqttServerComm("node", client, new MqttConnectOptions(), timer); + comm = com.bencodez.simpleapi.tests.support.TestReflection.construct( + MqttServerComm.class, "node", client, new MqttConnectOptions(), timer); ArgumentCaptor captured = ArgumentCaptor.forClass(MqttCallback.class); verify(client).setCallback(captured.capture()); callback = (MqttCallbackExtended) captured.getValue(); diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/mqtt/MqttServerCommRecoveryTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/mqtt/MqttServerCommRecoveryTest.java similarity index 97% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/mqtt/MqttServerCommRecoveryTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/mqtt/MqttServerCommRecoveryTest.java index b7e6e580..50117ae5 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/servercomm/mqtt/MqttServerCommRecoveryTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/mqtt/MqttServerCommRecoveryTest.java @@ -1,4 +1,6 @@ -package com.bencodez.simpleapi.servercomm.mqtt; +package com.bencodez.simpleapi.tests.servercomm.mqtt; + +import com.bencodez.simpleapi.servercomm.mqtt.MqttServerComm; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/redis/RedisHandlerClientConfigTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/redis/RedisHandlerClientConfigTest.java new file mode 100644 index 00000000..291b340f --- /dev/null +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/servercomm/redis/RedisHandlerClientConfigTest.java @@ -0,0 +1,27 @@ +package com.bencodez.simpleapi.tests.servercomm.redis; + +import com.bencodez.simpleapi.servercomm.redis.RedisHandler; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class RedisHandlerClientConfigTest { + + @Test + void tlsCanBeEnabled() { + redis.clients.jedis.JedisClientConfig config = com.bencodez.simpleapi.tests.support.TestReflection.call( + RedisHandler.class, "buildClientConfig", "", "", 0, true); + + assertTrue(config.isSsl()); + assertEquals("HTTPS", config.getSslParameters().getEndpointIdentificationAlgorithm()); + } + + @Test + void tlsRemainsDisabledByDefault() { + assertFalse(((redis.clients.jedis.JedisClientConfig) com.bencodez.simpleapi.tests.support.TestReflection.call( + RedisHandler.class, "buildClientConfig", "", "", 0, false)).isSsl()); + } +} diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/AbstractSqlTableInitializationTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/AbstractSqlTableInitializationTest.java similarity index 96% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/AbstractSqlTableInitializationTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/AbstractSqlTableInitializationTest.java index 38597008..8dec0eb3 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/AbstractSqlTableInitializationTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/AbstractSqlTableInitializationTest.java @@ -1,4 +1,9 @@ -package com.bencodez.simpleapi.sql.mysql; +package com.bencodez.simpleapi.tests.sql.mysql; + +import com.bencodez.simpleapi.sql.mysql.AbstractSqlTable; +import com.bencodez.simpleapi.sql.mysql.ConnectionManager; +import com.bencodez.simpleapi.sql.mysql.DbType; +import com.bencodez.simpleapi.sql.mysql.MySQL; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/ConnectionManagerPoolTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/ConnectionManagerPoolTest.java similarity index 98% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/ConnectionManagerPoolTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/ConnectionManagerPoolTest.java index 747933e7..8ada3d07 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/ConnectionManagerPoolTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/ConnectionManagerPoolTest.java @@ -1,4 +1,8 @@ -package com.bencodez.simpleapi.sql.mysql; +package com.bencodez.simpleapi.tests.sql.mysql; + +import com.bencodez.simpleapi.sql.mysql.ConnectionManager; +import com.bencodez.simpleapi.sql.mysql.DbType; +import com.bencodez.simpleapi.sql.mysql.MySQL; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbDriverFallbackTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/MariaDbDriverFallbackTest.java similarity index 84% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbDriverFallbackTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/MariaDbDriverFallbackTest.java index 9cc3741e..1b2ee5b9 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbDriverFallbackTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/MariaDbDriverFallbackTest.java @@ -1,4 +1,7 @@ -package com.bencodez.simpleapi.sql.mysql; +package com.bencodez.simpleapi.tests.sql.mysql; + +import com.bencodez.simpleapi.sql.mysql.ConnectionManager; +import com.bencodez.simpleapi.sql.mysql.DbType; import static org.junit.jupiter.api.Assertions.*; @@ -18,7 +21,7 @@ class MariaDbDriverFallbackTest { manager.setUseSSL(true); manager.setPublicKeyRetrieval(true); manager.setStr("&customOption=retained"); - String url = manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver"); + String url = SqlTestAccess.jdbcUrl(manager, "com.mysql.cj.jdbc.Driver"); assertTrue(url.startsWith("jdbc:mysql://localhost:3306/votes?")); assertTrue(url.contains("useSSL=true")); assertTrue(url.contains("allowPublicKeyRetrieval=true")); @@ -28,20 +31,20 @@ class MariaDbDriverFallbackTest { } @Test void nativeMariaDbDriverRetainsMariaDbScheme() { - assertTrue(manager(DbType.MARIADB).buildJdbcUrl("org.mariadb.jdbc.Driver").startsWith("jdbc:mariadb:")); + assertTrue(SqlTestAccess.jdbcUrl(manager(DbType.MARIADB), "org.mariadb.jdbc.Driver").startsWith("jdbc:mariadb:")); } @Test void explicitMariaDbDriverOverridesMysqlTypeAsBefore() { - assertTrue(manager(DbType.MYSQL).buildJdbcUrl("org.mariadb.jdbc.Driver").startsWith("jdbc:mariadb:")); + assertTrue(SqlTestAccess.jdbcUrl(manager(DbType.MYSQL), "org.mariadb.jdbc.Driver").startsWith("jdbc:mariadb:")); } @Test void customMariaDbDriverRetainsConfiguredScheme() { - assertTrue(manager(DbType.MARIADB).buildJdbcUrl("example.CustomDriver").startsWith("jdbc:mariadb:")); + assertTrue(SqlTestAccess.jdbcUrl(manager(DbType.MARIADB), "example.CustomDriver").startsWith("jdbc:mariadb:")); } @Test void mysqlAndPostgresqlSelectionsRemainUnchanged() { - assertTrue(manager(DbType.MYSQL).buildJdbcUrl("com.mysql.cj.jdbc.Driver").startsWith("jdbc:mysql:")); - assertTrue(manager(DbType.POSTGRESQL).buildJdbcUrl("org.postgresql.Driver").startsWith("jdbc:postgresql:")); + assertTrue(SqlTestAccess.jdbcUrl(manager(DbType.MYSQL), "com.mysql.cj.jdbc.Driver").startsWith("jdbc:mysql:")); + assertTrue(SqlTestAccess.jdbcUrl(manager(DbType.POSTGRESQL), "org.postgresql.Driver").startsWith("jdbc:postgresql:")); } @Test void absentMariaDbDriverResolvesToDriverThatAcceptsGeneratedUrl() throws Exception { diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbFallbackTlsTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/MariaDbFallbackTlsTest.java similarity index 85% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbFallbackTlsTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/MariaDbFallbackTlsTest.java index 3e37925a..efab7132 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MariaDbFallbackTlsTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/MariaDbFallbackTlsTest.java @@ -1,4 +1,7 @@ -package com.bencodez.simpleapi.sql.mysql; +package com.bencodez.simpleapi.tests.sql.mysql; + +import com.bencodez.simpleapi.sql.mysql.ConnectionManager; +import com.bencodez.simpleapi.sql.mysql.DbType; import static org.junit.jupiter.api.Assertions.*; @@ -20,7 +23,7 @@ class MariaDbFallbackTlsTest { @TempDir Path directory; @Test void sslEnabledFallbackRequiresTls() { - assertTrue(fallback().buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=REQUIRED&requireSSL=true")); + assertTrue(SqlTestAccess.jdbcUrl(fallback(), "com.mysql.cj.jdbc.Driver").endsWith("&sslMode=REQUIRED&requireSSL=true")); } @Test void explicitStrongerModesRemainIntact() { @@ -28,10 +31,10 @@ class MariaDbFallbackTlsTest { ConnectionManager manager = fallback(); manager.setStr("&sslMode=" + mode); if (mode.equals("VERIFY_IDENTITY") && legacyDriver()) { - assertThrows(IllegalArgumentException.class, () -> manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver")); + assertThrows(IllegalArgumentException.class, () -> SqlTestAccess.jdbcUrl(manager, "com.mysql.cj.jdbc.Driver")); } else { String certificateFlag = mode.startsWith("VERIFY_") ? "&verifyServerCertificate=true" : ""; - assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=" + mode + "&requireSSL=true" + certificateFlag)); + assertTrue(SqlTestAccess.jdbcUrl(manager, "com.mysql.cj.jdbc.Driver").endsWith("&sslMode=" + mode + "&requireSSL=true" + certificateFlag)); } } } @@ -44,32 +47,32 @@ class MariaDbFallbackTlsTest { "&requireSSL=unknown", "&require%53SL=false"}) { ConnectionManager manager = fallback(); manager.setStr(options); - assertThrows(IllegalArgumentException.class, () -> manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver")); + assertThrows(IllegalArgumentException.class, () -> SqlTestAccess.jdbcUrl(manager, "com.mysql.cj.jdbc.Driver")); } } @Test void legacyCertificateVerificationRemainsEnabled() { ConnectionManager manager = fallback(); manager.setStr("&verifyServerCertificate=true&customOption=retained"); - assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=VERIFY_CA&requireSSL=true&verifyServerCertificate=true")); + assertTrue(SqlTestAccess.jdbcUrl(manager, "com.mysql.cj.jdbc.Driver").endsWith("&sslMode=VERIFY_CA&requireSSL=true&verifyServerCertificate=true")); } @Test void explicitEncryptionRetainsLegacyCertificateVerification() { ConnectionManager manager = fallback(); manager.setStr("&sslMode=REQUIRED&verifyServerCertificate=true"); - assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith( + assertTrue(SqlTestAccess.jdbcUrl(manager, "com.mysql.cj.jdbc.Driver").endsWith( "&sslMode=VERIFY_CA&requireSSL=true&verifyServerCertificate=true")); } @Test void ordinaryMysqlNativeMariaAndSslDisabledRemainUnchanged() { ConnectionManager manager = fallback(); - assertFalse(manager.buildJdbcUrl("org.mariadb.jdbc.Driver").contains("sslMode=")); + assertFalse(SqlTestAccess.jdbcUrl(manager, "org.mariadb.jdbc.Driver").contains("sslMode=")); manager.setDbType(DbType.MYSQL); - assertFalse(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").contains("sslMode=")); + assertFalse(SqlTestAccess.jdbcUrl(manager, "com.mysql.cj.jdbc.Driver").contains("sslMode=")); manager.setDbType(DbType.MARIADB); manager.setUseSSL(false); manager.setStr("&sslMode=DISABLED"); - assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").endsWith("&sslMode=DISABLED")); + assertTrue(SqlTestAccess.jdbcUrl(manager, "com.mysql.cj.jdbc.Driver").endsWith("&sslMode=DISABLED")); } @Test void actualConnectorRejectsNonTlsGreetingBeforeSendingCredentials() throws Exception { @@ -93,7 +96,7 @@ class MariaDbFallbackTlsTest { properties.setProperty("user", "test-user"); properties.setProperty("password", "test-only-not-a-secret"); SQLException failure = assertThrows(SQLException.class, () -> - new com.mysql.cj.jdbc.Driver().connect(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver"), properties)); + new com.mysql.cj.jdbc.Driver().connect(SqlTestAccess.jdbcUrl(manager, "com.mysql.cj.jdbc.Driver"), properties)); assertTrue(failure.getMessage().contains("SSL"), failure::getMessage); assertEquals(-1, peer.get(5, TimeUnit.SECONDS), "No authentication packet may be sent to a non-TLS peer"); } finally { @@ -106,9 +109,9 @@ class MariaDbFallbackTlsTest { ConnectionManager manager = fallback(); manager.setStr("&sslMode=VERIFY_IDENTITY"); if (legacyDriver()) { - assertThrows(IllegalArgumentException.class, () -> manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver")); + assertThrows(IllegalArgumentException.class, () -> SqlTestAccess.jdbcUrl(manager, "com.mysql.cj.jdbc.Driver")); } else { - assertTrue(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver").contains("sslMode=VERIFY_IDENTITY")); + assertTrue(SqlTestAccess.jdbcUrl(manager, "com.mysql.cj.jdbc.Driver").contains("sslMode=VERIFY_IDENTITY")); } } @@ -150,7 +153,7 @@ class MariaDbFallbackTlsTest { properties.setProperty("user", "test-user"); properties.setProperty("password", "test-only-not-a-secret"); SQLException failure = assertThrows(SQLException.class, () -> - new com.mysql.cj.jdbc.Driver().connect(manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver"), properties)); + new com.mysql.cj.jdbc.Driver().connect(SqlTestAccess.jdbcUrl(manager, "com.mysql.cj.jdbc.Driver"), properties)); assertTrue(certificateFailure(failure), "Expected certificate validation failure, got " + failure); assertEquals(-1, peer.get(5, TimeUnit.SECONDS), "No authentication packet may reach an untrusted TLS peer"); } finally { diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MySQLDisconnectTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/MySQLDisconnectTest.java similarity index 95% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MySQLDisconnectTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/MySQLDisconnectTest.java index e03a4b8d..1b1c0e0e 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/MySQLDisconnectTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/MySQLDisconnectTest.java @@ -1,4 +1,7 @@ -package com.bencodez.simpleapi.sql.mysql; +package com.bencodez.simpleapi.tests.sql.mysql; + +import com.bencodez.simpleapi.sql.mysql.ConnectionManager; +import com.bencodez.simpleapi.sql.mysql.MySQL; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/PostgreSqlTlsConfigTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/PostgreSqlTlsConfigTest.java similarity index 94% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/PostgreSqlTlsConfigTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/PostgreSqlTlsConfigTest.java index 925566f7..d9803743 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/PostgreSqlTlsConfigTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/PostgreSqlTlsConfigTest.java @@ -1,4 +1,7 @@ -package com.bencodez.simpleapi.sql.mysql; +package com.bencodez.simpleapi.tests.sql.mysql; + +import com.bencodez.simpleapi.sql.mysql.MySQL; +import com.bencodez.simpleapi.sql.mysql.PostgreSqlTlsMode; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/PostgreSqlTlsModeTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/PostgreSqlTlsModeTest.java similarity index 72% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/PostgreSqlTlsModeTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/PostgreSqlTlsModeTest.java index ae7c119e..0c04d5a1 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/PostgreSqlTlsModeTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/PostgreSqlTlsModeTest.java @@ -1,4 +1,8 @@ -package com.bencodez.simpleapi.sql.mysql; +package com.bencodez.simpleapi.tests.sql.mysql; + +import com.bencodez.simpleapi.sql.mysql.ConnectionManager; +import com.bencodez.simpleapi.sql.mysql.DbType; +import com.bencodez.simpleapi.sql.mysql.PostgreSqlTlsMode; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -15,13 +19,13 @@ private ConnectionManager postgres() { @Test void legacyModePreservesBothUseSslSettingsAndRawLine() { ConnectionManager manager = postgres(); assertEquals("jdbc:postgresql://db.example.test:5432/votes?reWriteBatchedInserts=true", - manager.buildJdbcUrl("org.postgresql.Driver")); + SqlTestAccess.jdbcUrl(manager, "org.postgresql.Driver")); manager.setUseSSL(true); assertEquals("jdbc:postgresql://db.example.test:5432/votes?reWriteBatchedInserts=true&sslmode=require", - manager.buildJdbcUrl("org.postgresql.Driver")); + SqlTestAccess.jdbcUrl(manager, "org.postgresql.Driver")); manager.setStr("?sslmode=disable"); assertEquals("jdbc:postgresql://db.example.test:5432/votes?sslmode=disable&reWriteBatchedInserts=true&sslmode=require", - manager.buildJdbcUrl("org.postgresql.Driver")); + SqlTestAccess.jdbcUrl(manager, "org.postgresql.Driver")); } @Test void explicitModeControlsPostgresTlsRegardlessOfUseSsl() { @@ -30,17 +34,17 @@ private ConnectionManager postgres() { manager.setStr("?sslrootcert=/etc/db/ca.pem&applicationName=votes"); String verified = "jdbc:postgresql://db.example.test:5432/votes?sslrootcert=/etc/db/ca.pem" + "&applicationName=votes&reWriteBatchedInserts=true&sslmode=verify-full&gssEncMode=disable"; - assertEquals(verified, manager.buildJdbcUrl("org.postgresql.Driver")); + assertEquals(verified, SqlTestAccess.jdbcUrl(manager, "org.postgresql.Driver")); manager.setUseSSL(true); - assertEquals(verified, manager.buildJdbcUrl("org.postgresql.Driver")); + assertEquals(verified, SqlTestAccess.jdbcUrl(manager, "org.postgresql.Driver")); manager.setPostgreSqlTlsMode(PostgreSqlTlsMode.REQUIRE); manager.setStr("&applicationName=votes"); assertEquals("jdbc:postgresql://db.example.test:5432/votes?reWriteBatchedInserts=true&sslmode=require&gssEncMode=disable&applicationName=votes", - manager.buildJdbcUrl("org.postgresql.Driver")); + SqlTestAccess.jdbcUrl(manager, "org.postgresql.Driver")); manager.setPostgreSqlTlsMode(PostgreSqlTlsMode.DISABLE); manager.setStr(""); assertEquals("jdbc:postgresql://db.example.test:5432/votes?reWriteBatchedInserts=true&sslmode=disable", - manager.buildJdbcUrl("org.postgresql.Driver")); + SqlTestAccess.jdbcUrl(manager, "org.postgresql.Driver")); } @Test void explicitModeRejectsConflictingRawLineOptions() { @@ -50,26 +54,26 @@ private ConnectionManager postgres() { "?applicationName=votes&ssl=false", "&sslfactory=com.example.TrustAll", "sslhostnameverifier=com.example.TrustAll", "gssEncMode=prefer"}) { manager.setStr(line); - assertThrows(IllegalArgumentException.class, () -> manager.buildJdbcUrl("org.postgresql.Driver"), line); + assertThrows(IllegalArgumentException.class, () -> SqlTestAccess.jdbcUrl(manager, "org.postgresql.Driver"), line); } manager.setStr("?ssl%zz=disable"); - assertThrows(IllegalArgumentException.class, () -> manager.buildJdbcUrl("org.postgresql.Driver")); + assertThrows(IllegalArgumentException.class, () -> SqlTestAccess.jdbcUrl(manager, "org.postgresql.Driver")); } @Test void mysqlAndMariaDbKeepLegacyUrlsAndRejectPostgresModes() { ConnectionManager manager = postgres(); manager.setDbType(DbType.MYSQL); - String mysqlUrl = manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver"); + String mysqlUrl = SqlTestAccess.jdbcUrl(manager, "com.mysql.cj.jdbc.Driver"); assertEquals("jdbc:mysql://db.example.test:5432/votes?useSSL=false&allowMultiQueries=true" + "&rewriteBatchedStatements=true&useDynamicCharsetInfo=false&allowPublicKeyRetrieval=false" + "&tcpKeepAlive=true&connectTimeout=10000&socketTimeout=30000&serverTimezone=UTC", mysqlUrl); manager.setPostgreSqlTlsMode(PostgreSqlTlsMode.VERIFY_FULL); - assertThrows(IllegalArgumentException.class, () -> manager.buildJdbcUrl("com.mysql.cj.jdbc.Driver")); + assertThrows(IllegalArgumentException.class, () -> SqlTestAccess.jdbcUrl(manager, "com.mysql.cj.jdbc.Driver")); manager.setDbType(DbType.MARIADB); - assertThrows(IllegalArgumentException.class, () -> manager.buildJdbcUrl("org.mariadb.jdbc.Driver")); + assertThrows(IllegalArgumentException.class, () -> SqlTestAccess.jdbcUrl(manager, "org.mariadb.jdbc.Driver")); manager.setPostgreSqlTlsMode(PostgreSqlTlsMode.LEGACY); assertEquals(mysqlUrl.replace("jdbc:mysql:", "jdbc:mariadb:"), - manager.buildJdbcUrl("org.mariadb.jdbc.Driver")); + SqlTestAccess.jdbcUrl(manager, "org.mariadb.jdbc.Driver")); } @Test void parsesModeNamesAndRejectsUnknownValues() { diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/SharedSqlTest.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/SharedSqlTest.java similarity index 91% rename from SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/SharedSqlTest.java rename to SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/SharedSqlTest.java index 1d194402..15fdcc8f 100644 --- a/SimpleAPI/src/test/java/com/bencodez/simpleapi/sql/mysql/SharedSqlTest.java +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/SharedSqlTest.java @@ -1,4 +1,8 @@ -package com.bencodez.simpleapi.sql.mysql; +package com.bencodez.simpleapi.tests.sql.mysql; + +import com.bencodez.simpleapi.sql.mysql.AbstractSqlTable; +import com.bencodez.simpleapi.sql.mysql.ConnectionManager; +import com.bencodez.simpleapi.sql.mysql.DbType; import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test; diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/SqlTestAccess.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/SqlTestAccess.java new file mode 100644 index 00000000..09c2bab3 --- /dev/null +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/sql/mysql/SqlTestAccess.java @@ -0,0 +1,12 @@ +package com.bencodez.simpleapi.tests.sql.mysql; + +import com.bencodez.simpleapi.sql.mysql.ConnectionManager; +import com.bencodez.simpleapi.tests.support.TestReflection; + +/** Exercises the original package-private URL builder without changing its visibility. */ +final class SqlTestAccess { + private SqlTestAccess() { } + static String jdbcUrl(ConnectionManager manager, String driver) { + return TestReflection.call(manager, "buildJdbcUrl", driver); + } +} diff --git a/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/support/TestReflection.java b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/support/TestReflection.java new file mode 100644 index 00000000..21ee095d --- /dev/null +++ b/SimpleAPI/src/test/java/com/bencodez/simpleapi/tests/support/TestReflection.java @@ -0,0 +1,58 @@ +package com.bencodez.simpleapi.tests.support; + +import java.lang.reflect.*; +import java.util.*; + +/** Test-only access to the original implementation without expanding production visibility. */ +public final class TestReflection { + private TestReflection() { } + public interface Handle { Object target(); } + public static Class type(String name) { + try { return Class.forName(name); } + catch (ClassNotFoundException failure) { throw new AssertionError(failure); } + } + public static Object unwrap(Object value) { + if (value instanceof Handle handle) return handle.target(); + if (value instanceof Collection values) return values.stream().map(TestReflection::unwrap).toList(); + return value; + } + public static Object field(Class type, Object target, String name) { + try { Field field = type.getDeclaredField(name); field.setAccessible(true); return field.get(unwrap(target)); } + catch (ReflectiveOperationException failure) { throw new AssertionError(failure); } + } + @SuppressWarnings("unchecked") + public static T call(Object target, String name, Object... arguments) { + Class type = target instanceof Class cls ? cls : unwrap(target).getClass(); + Object receiver = target instanceof Class ? null : unwrap(target); + Object[] values = Arrays.stream(arguments).map(TestReflection::unwrap).toArray(); + List matches = Arrays.stream(type.getDeclaredMethods()) + .filter(method -> method.getName().equals(name) && matches(method.getParameterTypes(), values)).toList(); + if (matches.size() != 1) throw new AssertionError("Expected one method: " + type + "." + name + ": " + matches); + try { Method method = matches.get(0); method.setAccessible(true); return (T) method.invoke(receiver, values); } + catch (InvocationTargetException failure) { return TestReflection.rethrow(failure.getCause()); } + catch (ReflectiveOperationException failure) { throw new AssertionError(failure); } + } + @SuppressWarnings("unchecked") + public static T construct(Class type, Object... arguments) { + Object[] values = Arrays.stream(arguments).map(TestReflection::unwrap).toArray(); + List> matches = Arrays.stream(type.getDeclaredConstructors()) + .filter(constructor -> matches(constructor.getParameterTypes(), values)).toList(); + if (matches.size() != 1) throw new AssertionError("Expected one constructor: " + type + ": " + matches); + try { Constructor constructor = matches.get(0); constructor.setAccessible(true); return (T) constructor.newInstance(values); } + catch (InvocationTargetException failure) { return TestReflection.rethrow(failure.getCause()); } + catch (ReflectiveOperationException failure) { throw new AssertionError(failure); } + } + private static boolean matches(Class[] parameters, Object[] values) { + if (parameters.length != values.length) return false; + for (int i = 0; i < parameters.length; i++) { + Class type = parameters[i]; + if (type == int.class) type = Integer.class; + else if (type == long.class) type = Long.class; + else if (type == boolean.class) type = Boolean.class; + if (values[i] == null ? parameters[i].isPrimitive() : !type.isInstance(values[i])) return false; + } + return true; + } + @SuppressWarnings("unchecked") + private static T rethrow(Throwable failure) throws E { throw (E) failure; } +}