diff --git a/VotingPlugin/pom.xml b/VotingPlugin/pom.xml index d2da72925..e588f8a48 100644 --- a/VotingPlugin/pom.xml +++ b/VotingPlugin/pom.xml @@ -271,7 +271,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.5.4 + 3.6.1 default-test @@ -408,7 +408,7 @@ org.yaml snakeyaml - 2.6 + 2.7 org.spigotmc @@ -431,7 +431,7 @@ com.google.code.gson gson - 2.14.0 + 2.15.0 provided @@ -482,7 +482,7 @@ redis.clients jedis - 8.0.1 + 8.0.2 me.clip diff --git a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/packaging/PackagedArtifactTest.java b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/packaging/PackagedArtifactTest.java index 9ca757675..4c8303601 100644 --- a/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/packaging/PackagedArtifactTest.java +++ b/VotingPlugin/src/test/java/com/bencodez/votingplugin/tests/packaging/PackagedArtifactTest.java @@ -1,6 +1,8 @@ package com.bencodez.votingplugin.tests.packaging; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -8,12 +10,14 @@ import java.io.ByteArrayOutputStream; import java.io.PrintStream; +import java.io.StringWriter; import java.net.URL; import java.net.URLClassLoader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.jar.JarFile; +import javax.tools.ToolProvider; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -119,6 +123,125 @@ void packagedRedisClientLinksWithModuleApis() throws Exception { } } + @Test + void packagedJedisUsesTheResolvedClientRatherThanAnEmbeddedProducerCopy() throws Exception { + Path selected = Path.of(redis.clients.jedis.Jedis.class.getProtectionDomain() + .getCodeSource().getLocation().toURI()); + try (JarFile dependency = new JarFile(selected.toFile()); + JarFile artifact = new JarFile(packagedJar().toFile())) { + // MultiDbClient differs across the 8.0.1/8.0.2 patch boundary; Jedis alone does not. + for (String name : java.util.List.of("redis/clients/jedis/Jedis.class", + "redis/clients/jedis/JedisPool.class", "redis/clients/jedis/MultiDbClient.class")) { + assertNotNull(dependency.getEntry(name), "Missing resolved client API: " + name); + assertNotNull(artifact.getEntry(name), "Missing packaged client API: " + name); + assertEquals(1L, artifact.stream().filter(entry -> entry.getName().equals(name)).count(), + "Duplicate packaged client: " + name); + try (var expected = dependency.getInputStream(dependency.getEntry(name)); + var actual = artifact.getInputStream(artifact.getEntry(name))) { + assertArrayEquals(expected.readAllBytes(), actual.readAllBytes(), + "A producer's embedded client must not shadow the resolved dependency: " + name); + } + } + } + } + + @Test + void packagedSimpleApiRedisHandlerUsesTheSelectedClientWithoutMavenDependencies(@TempDir Path directory) + throws Exception { + assertRedisHandlerLinks(packagedJar(), + "com.bencodez.votingplugin.simpleapi.servercomm.redis.RedisHandler", directory); + } + + @Test + void publishedProducerArtifactsRetainLegacyRedisApisAndLinkTheirHandlers(@TempDir Path directory) throws Exception { + var resources = PackagedArtifactTest.class.getClassLoader() + .getResources("com/bencodez/simpleapi/servercomm/redis/RedisHandler.class"); + java.util.Set producers = new java.util.LinkedHashSet<>(); + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + assertEquals("jar", resource.getProtocol(), "Producer verification requires actual dependency JARs"); + var connection = (java.net.JarURLConnection) resource.openConnection(); + connection.setUseCaches(false); + producers.add(connection.getJarFileURL()); + } + assertEquals(2, producers.size(), "Verify both the normal AdvancedCore and direct SimpleAPI artifacts"); + int index = 0; + URL platformSlf4j = org.slf4j.Logger.class.getProtectionDomain().getCodeSource().getLocation(); + for (URL producer : producers) { + try (URLClassLoader loader = new URLClassLoader(new URL[] { producer, platformSlf4j }, + ClassLoader.getPlatformClassLoader())) { + Class endpoint = Class.forName("redis.clients.jedis.HostAndPort", true, loader); + Class config = Class.forName("redis.clients.jedis.JedisClientConfig", true, loader); + Class.forName("redis.clients.jedis.JedisPooled", true, loader).getConstructor(endpoint, config); + Class.forName("redis.clients.jedis.JedisSentineled", true, loader) + .getConstructor(String.class, config, java.util.Set.class, config); + Class.forName("redis.clients.jedis.UnifiedJedis", true, loader).getConstructor(endpoint, config); + } + assertRedisHandlerLinks(Path.of(producer.toURI()), "com.bencodez.simpleapi.servercomm.redis.RedisHandler", + Files.createDirectory(directory.resolve("producer-" + index++))); + } + } + + private static void assertRedisHandlerLinks(Path artifact, String handlerName, Path directory) throws Exception { + Path source = directory.resolve("PackagedRedisHandlerProbe.java"); + Files.writeString(source, """ + public final class PackagedRedisHandlerProbe + extends %s { + public PackagedRedisHandlerProbe(boolean tls) { + super("localhost", 0, "", "", 3, tls); + } + @Override public void debug(String message) { } + public Object listener() { + return createEnvelopeListener("synthetic", (channel, envelope) -> { }); + } + } + """.formatted(handlerName)); + var compiler = ToolProvider.getSystemJavaCompiler(); + assertNotNull(compiler); + StringWriter diagnostics = new StringWriter(); + try (var files = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)) { + assertTrue(compiler.getTask(diagnostics, files, null, + java.util.List.of("--release", "21", "-proc:none", "-classpath", artifact.toString(), + "-d", directory.toString()), null, + files.getJavaFileObjects(source.toFile())).call(), diagnostics.toString()); + } + URL platformSlf4j = org.slf4j.Logger.class.getProtectionDomain().getCodeSource().getLocation(); + try (URLClassLoader loader = new URLClassLoader(new URL[] { + directory.toUri().toURL(), artifact.toUri().toURL(), platformSlf4j }, + ClassLoader.getPlatformClassLoader())) { + Class probe = Class.forName("PackagedRedisHandlerProbe", true, loader); + Class handler = probe.getSuperclass(); + Class configApi = Class.forName("redis.clients.jedis.JedisClientConfig", true, loader); + Class subscriber = Class.forName("redis.clients.jedis.JedisPubSub", true, loader); + Class jedis = Class.forName("redis.clients.jedis.Jedis", true, loader); + jedis.getMethod("publish", String.class, String.class); + jedis.getMethod("subscribe", subscriber, String[].class); + var configField = handler.getDeclaredField("clientConfig"); + configField.setAccessible(true); + var poolField = handler.getDeclaredField("publisherPool"); + poolField.setAccessible(true); + for (boolean tls : new boolean[] { false, true }) { + Object instance = probe.getConstructor(boolean.class).newInstance(tls); + Object pool = poolField.get(instance); + try { + Object config = configField.get(instance); + assertEquals(tls, configApi.getMethod("isSsl").invoke(config)); + assertEquals(3, configApi.getMethod("getDatabase").invoke(config)); + if (tls) { + var parameters = (javax.net.ssl.SSLParameters) configApi.getMethod("getSslParameters").invoke(config); + assertEquals("HTTPS", parameters.getEndpointIdentificationAlgorithm()); + } + assertTrue(subscriber.isInstance(probe.getMethod("listener").invoke(instance))); + assertEquals(false, pool.getClass().getMethod("isClosed").invoke(pool)); + } finally { + // Constructors are lazy: no publish, subscribe or connection borrow is performed. + handler.getMethod("close").invoke(instance); + } + assertEquals(true, pool.getClass().getMethod("isClosed").invoke(pool)); + } + } + } + @Test void packagedJdkTlsIdentityWorksWithoutExternalCrypto(@TempDir Path directory) throws Exception { URL jar = packagedJar().toUri().toURL(); diff --git a/docs/redis-dependency-compatibility.md b/docs/redis-dependency-compatibility.md new file mode 100644 index 000000000..a576a5c6a --- /dev/null +++ b/docs/redis-dependency-compatibility.md @@ -0,0 +1,11 @@ +# Redis dependency compatibility + +The dependency versions in SimpleAPI, AdvancedCore and VotingPlugin describe different packaging boundaries. Equal version strings alone do not prove that a consumer can link or that a shaded JAR contains the intended implementation. + +- SimpleAPI's full artifact preserves the established Jedis 7 API, including `JedisPooled`, `JedisSentineled` and the public `UnifiedJedis` constructors. Its existing full-artifact test compiles and links a legacy consumer using only that JAR. Jedis 8 removes those APIs; replacing the producer dependency without a migration would break the full-artifact contract. +- AdvancedCore excludes the ordinary transitive Jedis dependency from SimpleAPI. This exclusion does not strip classes already embedded in SimpleAPI's full JAR. The published AdvancedCore artifact therefore still contains that producer's Redis implementation. +- VotingPlugin filters `redis/clients/**` from both producer JARs and explicitly selects its own Jedis client. These filters must remain together: removing only a Maven transitive dependency does not prevent a fat producer JAR from shadowing the selected client. + +VotingPlugin's package checks verify the selected implementation's class bytes, including an anchor that distinguishes the 8.0.1 and 8.0.2 patch artifacts. They compile and load the relocated SimpleAPI Redis handler using only the downloadable plugin JAR and the platform logging API. The same constructor, listener, TLS configuration and close checks run separately against the actual normal AdvancedCore and SimpleAPI dependency JARs; their legacy constructors remain checked in isolated class loaders. Maven's test dependency classpath cannot supply a missing Redis class to these probes. + +Keep the SimpleAPI legacy exclusion until a separately reviewed compatibility-preserving migration exists. Evaluate each dependency update against its actual public API, supported Java version, resolved artifacts and downstream packaged linkage. A passed constructor/linkage check is not a Redis broker integration test: the probes do not publish, subscribe, borrow a connection or start a service. Native and live transport acceptance remain separate.