test: qualify Redis certificate replacement on restart

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
james.maesandCodex committed 2026-10-22 17:01:10 -05:00
1 parent 2e7ad97f3f
commit 54adcfed62
2 files changed
+47 -2

No files matched your search

+12 -2
View File
@@ -32,8 +32,18 @@ verify authenticated TLS, untrusted-certificate rejection, wrong-host rejection,
and no plaintext fallback. Mutual-TLS tests require a separate trusted client
identity, reject missing or untrusted client certificates, and verify framework
health after a successful authenticated connection. Certificates and keys are
generated only for the disposable fixture. Certificate rotation and failover
acceptance remain open; only standalone endpoints are implemented.
generated only for the disposable fixture.
The TLS provider is resolved when the Redis runtime starts. Replacing its context
alone does not reload existing pools or subscribers. For certificate replacement,
stop dependent services and restart the Redis runtime through the application
lifecycle, then restart its dependents. Qualification replaces both server and
client certificates on the same Redis endpoint: stale material fails a fresh
handshake, health becomes DOWN, and runtime restart with updated material restores
PONG and health UP while closing the old pool. This is a restart procedure, not
zero-downtime certificate rotation. Live reload, trust-chain/revocation policies,
subscriber recovery across certificate changes and failover acceptance remain open;
only standalone endpoints are implemented.
QQQ's application launcher starts the registered runtime service. Startup validates
configuration and pings every connection before exposing any of the named pools.
@@ -32,6 +32,7 @@ import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import redis.clients.jedis.Protocol;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.testcontainers.containers.GenericContainer;
@@ -146,6 +147,40 @@ class RedisTlsTest
assertEquals("PONG", RedisRuntimeService.connection(instance, "presence").client().ping());
assertEquals(com.kingsrook.qqq.backend.core.health.HealthStatus.UP, new RedisHealthCheck().run(
new com.kingsrook.qqq.backend.core.health.HealthCheckInput().withQInstance(instance)).getStatus());
// Rotate both identities on the same endpoint; existing TLS sessions do not re-handshake.
Path rotatedServer = Files.createDirectory(temporary.resolve("rotated-server"));
Path rotatedClient = Files.createDirectory(temporary.resolve("rotated-client"));
certificate(rotatedServer, identity);
certificate(rotatedClient, "dns:rotated-client-fixture.invalid");
redis.copyFileToContainer(MountableFile.forHostPath(rotatedServer.resolve("cert.pem"), 0444), "/rotated-server-cert.pem");
redis.copyFileToContainer(MountableFile.forHostPath(rotatedServer.resolve("key.pem"), 0444), "/rotated-server-key.pem");
redis.copyFileToContainer(MountableFile.forHostPath(rotatedClient.resolve("cert.pem"), 0444), "/rotated-client-trust.pem");
var oldClient = RedisRuntimeService.connection(instance, "presence").client();
var configured = oldClient.sendCommand(Protocol.Command.CONFIG, "SET",
"tls-cert-file", "/rotated-server-cert.pem", "tls-key-file", "/rotated-server-key.pem",
"tls-ca-cert-file", "/rotated-client-trust.pem");
assertEquals("OK", new String((byte[]) configured, java.nio.charset.StandardCharsets.UTF_8));
oldClient.getPool().clear();
assertThrows(redis.clients.jedis.exceptions.JedisException.class, oldClient::ping,
"A fresh connection with stale trust and client keys must fail");
assertEquals(0, oldClient.getPool().getNumActive());
var healthInput = new com.kingsrook.qqq.backend.core.health.HealthCheckInput().withQInstance(instance);
assertEquals(com.kingsrook.qqq.backend.core.health.HealthStatus.DOWN, new RedisHealthCheck().run(healthInput).getStatus());
TrustedContext.context = clientContext(rotatedServer, rotatedClient);
assertThrows(redis.clients.jedis.exceptions.JedisException.class, oldClient::ping,
"Changing the provider alone must not be mistaken for live TLS reload");
runtime.stop();
runtime.afterApplicationStop();
assertTrue(oldClient.getPool().isClosed());
assertThrows(IllegalStateException.class, () -> RedisRuntimeService.connection(instance, "presence"));
runtime.start(instance);
var newClient = RedisRuntimeService.connection(instance, "presence").client();
assertNotSame(oldClient, newClient);
assertEquals("PONG", newClient.ping());
assertEquals(com.kingsrook.qqq.backend.core.health.HealthStatus.UP, new RedisHealthCheck().run(healthInput).getStatus());
assertEquals(0, newClient.getPool().getNumActive());
}
finally
{