Integration Tests¶
Table of Contents¶
- Overview
- Running Integration Tests
- Debugging Integration Tests
- Test Categories
- Writing New Integration Tests
- Container File Mounting Best Practices
- Troubleshooting
Overview¶
Integration tests verify that the kete provider works correctly when deployed inside a real Keycloak instance. These tests use Testcontainers to spin up Docker containers for Keycloak and supporting infrastructure (MockServer for HTTP destinations, etc.).
Prerequisites¶
- Docker must be running
- Maven must be installed
- Project must be compiled (
mvn compile)
Running Integration Tests¶
Run All Tests (Unit + Integration)¶
Run Only Integration Tests¶
Run Only Unit Tests¶
Run Tests with Coverage¶
Debugging Integration Tests¶
There are several approaches to debug your EventListener provider code during integration tests.
Option 1: Remote Debugging (Attach Debugger to Keycloak Container)¶
The Keycloak Testcontainer supports remote debugging. Add .withDebugFixedPort(5005, true) to suspend Keycloak until you attach a debugger:
@Container
static KeycloakContainer keycloak = new KeycloakContainer(TestUtils.KEYCLOAK_IMAGE)
.withDefaultProviderClasses()
.withDebugFixedPort(5005, true) // Enable debug on port 5005, suspend until attached
// ... other configuration
Steps:
- Modify the test to add
.withDebugFixedPort(5005, true) - Run the test - it will pause waiting for debugger
- In VS Code, create a debug configuration:
- Start the debug session - Keycloak will resume and hit your breakpoints
Option 2: Debug Without Suspend¶
Use .withDebug() to enable debugging without suspending:
@Container
static KeycloakContainer keycloak = new KeycloakContainer(TestUtils.KEYCLOAK_IMAGE)
.withDefaultProviderClasses()
.withDebug() // Debug port on random port, no suspend
Get the debug port with keycloak.getMappedPort(5005).
Option 3: Embedded Undertow (Keycloak Arquillian)¶
For deep debugging where you need to run Keycloak in the same JVM, you can use Keycloak's Arquillian testsuite with embedded Undertow:
This runs Keycloak on embedded Undertow in the same JVM as your tests.
Test Categories¶
Keycloak Boot Tests¶
Located in: src/tests/integration-tests/.../keycloak/CleanKeycloakBootTests.java
Verifies basic Keycloak container functionality: - Container starts successfully - Admin client can connect - Realms are accessible
Provider Integration Tests¶
Located in: src/tests/integration-tests/.../provider/EventListenerProviderIntegrationTests.java
Verifies the kete provider: - Provider loads into Keycloak - Events are forwarded to destinations - Serialization works correctly - Admin events are captured
Writing New Integration Tests¶
Basic Template¶
@Testcontainers
class MyIntegrationTests {
static Network network = Network.newNetwork();
@Container
static KeycloakContainer keycloak = new KeycloakContainer(TestUtils.KEYCLOAK_IMAGE)
.withNetwork(network)
.withDefaultProviderClasses()
.withEnv("enabled", "true")
.withEnv("kete.routes.my-route.realm-matchers.filter", "list:test")
.withEnv("kete.routes.my-route.destination.kind", "http")
.withEnv("kete.routes.my-route.destination.url", "http://destination:8080")
.withEnv("kete.routes.my-route.serializer.kind", "json");
@Test
void myTest() {
// Use adminClient to interact with Keycloak
// Verify events are forwarded to destinations
}
}
Mounting Configuration Files¶
CRITICAL: When tests require custom configuration files for containers (broker configs, certificates, etc.), ALWAYS use in-memory Transferable.of() with 0777 permissions. NEVER use withFileSystemBind() or withCopyToContainer() without permissions.
// Step 1: Read file content into memory
var brokerXmlBytes = Files.readAllBytes(Path.of(brokerXmlPath));
// Step 2: Copy to container memory with full permissions
@Container
static GenericContainer<?> broker = new GenericContainer<>(imageName)
.withCopyToContainer(Transferable.of(brokerXmlBytes, 0777), "/etc/broker.xml");
See Container File Mounting Best Practices for complete details.
Key Configuration Environment Variables¶
| Variable | Description | Example |
|---|---|---|
enabled |
Enable the provider | true |
kete.routes.<name>.realm-matchers.<name> |
Realm filter | list:master |
kete.routes.<name>.destination.kind |
Destination kind | http, kafka, amqp-0.9.1 |
kete.routes.<name>.destination.url |
Destination URL | http://server:8080/events |
kete.routes.<name>.serializer.kind |
Serialization format | json, xml, yaml |
Adding Destination Containers¶
For Kafka:
@Container
static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0"))
.withNetwork(network)
.withNetworkAliases("kafka");
For RabbitMQ:
@Container
static RabbitMQContainer rabbitmq = new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.12-management"))
.withNetwork(network)
.withNetworkAliases("rabbitmq");
Container File Mounting Best Practices¶
Why In-Memory Transfer with Full Permissions?¶
ALWAYS use withCopyToContainer(Transferable.of(bytes, 0777)) for mounting files into containers. NEVER use withFileSystemBind().
| Aspect | In-Memory Transfer (Transferable.of()) |
File System Bind (withFileSystemBind) |
|---|---|---|
| GitHub Actions | ✅ Works reliably in CI/CD | ❌ Fails due to filesystem limitations |
| Permissions | Full control with 0777 parameter | Unpredictable based on host OS |
| Cross-platform | Identical behavior on all OS | Different behavior Windows/Linux/macOS |
| Performance | Fast in-memory copy | Filesystem mount overhead |
| Best Practice | REQUIRED in this codebase | FORBIDDEN |
Standard In-Memory Transfer Pattern¶
Use this pattern consistently across all TestBase classes:
// Step 1: Read file content into byte array
var configBytes = Files.readAllBytes(Path.of(sourceConfigPath));
var keystoreBytes = Files.readAllBytes(Path.of(sourceKeystorePath));
var truststoreBytes = Files.readAllBytes(Path.of(sourceTruststorePath));
// Step 2: Copy to container memory with 0777 permissions
container = new GenericContainer<>(imageName)
.withCopyToContainer(Transferable.of(configBytes, 0777), "/container/path/config.xml")
.withCopyToContainer(Transferable.of(keystoreBytes, 0777), "/container/path/keystore.jks")
.withCopyToContainer(Transferable.of(truststoreBytes, 0777), "/container/path/truststore.jks");
Complete Example: AMQP1 with TLS¶
From io.github.fortunen.kete.integrationtests.amqp1destination.TestBase:
private void startActiveMqArtemisWithTls(TlsMaterial tls, boolean requireClientAuth) throws Exception {
// Create broker configuration XML as string
var brokerXml = createArtemisBrokerXml(
tls.getKeyStorePassword(),
tls.getTrustStorePassword(),
requireClientAuth
);
// Read certificate files into memory
var keystoreBytes = Files.readAllBytes(Path.of(tls.getServerKeyStoreFilePath()));
var truststoreBytes = Files.readAllBytes(Path.of(tls.getTrustStoreFilePath()));
// Copy all files to container memory with 0777 permissions
container = new GenericContainer<>(DockerImageName.parse("apache/activemq-artemis:2.40.0-alpine"))
.withEnv("ARTEMIS_USER", DEFAULT_USERNAME)
.withEnv("ARTEMIS_PASSWORD", DEFAULT_PASSWORD)
.withEnv("ANONYMOUS_LOGIN", "true")
.withCopyToContainer(
Transferable.of(brokerXml.getBytes(StandardCharsets.UTF_8), 0777),
"/var/lib/artemis-instance/etc-override/broker.xml")
.withCopyToContainer(
Transferable.of(keystoreBytes, 0777),
"/var/lib/artemis-instance/etc-override/keystore.jks")
.withCopyToContainer(
Transferable.of(truststoreBytes, 0777),
"/var/lib/artemis-instance/etc-override/truststore.jks")
.withExposedPorts(AMQP_PORT, AMQPS_PORT, 8161)
.waitingFor(Wait.forLogMessage(".*AMQ221007.*", 1))
.withStartupTimeout(Duration.ofMinutes(10));
container.start();
}
Inline Content Pattern¶
For configuration generated as strings (XML, YAML, TOML, properties):
// Create config content as string
var mosquittoConf = """
listener 1883
allow_anonymous true
""";
mosquitto = new GenericContainer<>(DockerImageName.parse("eclipse-mosquitto:2.0"))
.withNetwork(createNetwork())
.withNetworkAliases("mosquitto")
.withExposedPorts(MQTT_PORT)
.withCommand("mosquitto", "-c", "/mosquitto-no-auth.conf")
.withCopyToContainer(
Transferable.of(mosquittoConf.getBytes(StandardCharsets.UTF_8), 0777),
"/mosquitto-no-auth.conf");
Key Implementation Details¶
- Always specify 0777 permissions:
Transferable.of(content, 0777)ensures maximum compatibility - Read files into memory: Use
Files.readAllBytes(Path.of(path))for binary files - Convert strings to bytes: Use
.getBytes(StandardCharsets.UTF_8)for text content - Import Transferable:
import org.testcontainers.utility.MountableFile.Transferable; - No cleanup needed: In-memory content is garbage collected automatically
Pattern Variations by Use Case¶
Multiple Configuration Files (STOMP, AMQP1, WebSocket)¶
var activeMqXml = createActiveMqConfig();
var keyStoreBytes = Files.readAllBytes(Path.of(tls.getServerKeyStoreFilePath()));
var trustStoreBytes = Files.readAllBytes(Path.of(tls.getTrustStoreFilePath()));
container
.withCopyToContainer(Transferable.of(activeMqXml.getBytes(UTF_8), 0777), "/conf/activemq.xml")
.withCopyToContainer(Transferable.of(keyStoreBytes, 0777), "/conf/keystore.jks")
.withCopyToContainer(Transferable.of(trustStoreBytes, 0777), "/conf/truststore.jks");
Single Config File (MQTT E2E)¶
var mosquittoConf = "listener 1883\nallow_anonymous true\n";
container.withCopyToContainer(
Transferable.of(mosquittoConf.getBytes(UTF_8), 0777),
"/mosquitto-no-auth.conf");
TLS Certificates (Pulsar, NATS, Redis)¶
var certBytes = Files.readAllBytes(Path.of(tls.getServerCertificatePemFilePath()));
var keyBytes = Files.readAllBytes(Path.of(tls.getServerPrivateKeyPemFilePath()));
var caBytes = Files.readAllBytes(Path.of(tls.getCaCertificatePemFilePath()));
container
.withCopyToContainer(Transferable.of(certBytes, 0777), "/certs/server.crt")
.withCopyToContainer(Transferable.of(keyBytes, 0777), "/certs/server.key")
.withCopyToContainer(Transferable.of(caBytes, 0777), "/certs/ca.crt");
Common Pitfalls to Avoid¶
| Don't | ✅ Do |
|---|---|
withFileSystemBind(hostPath, "/path", BindMode.READ_ONLY) |
withCopyToContainer(Transferable.of(bytes, 0777), "/path") |
Transferable.of(content) without permissions |
Transferable.of(content, 0777) |
withCopyToContainer(Transferable.of(bytes), "/path") |
withCopyToContainer(Transferable.of(bytes, 0777), "/path") |
| Create temp files on disk | Read directly into memory with Files.readAllBytes() |
Use BindMode.READ_ONLY |
Always use 0777 permissions for maximum compatibility |
Why This Matters¶
- GitHub Actions Compatibility: Eliminates filesystem mounting issues in CI/CD
- Cross-platform: Identical behavior on Windows, Linux, macOS
- Permissions: 0777 ensures containers can read/write/execute without issues
- Simplicity: No temp file cleanup needed, automatic garbage collection
- Reliability: Consistent pattern across all test classes reduces bugs
Troubleshooting¶
Container Fails to Start¶
- Check Docker is running
- Check for port conflicts
- Increase container startup timeout:
Provider Not Loading¶
- Ensure
mvn compilewas run before tests - Check the provider is correctly registered in
META-INF/services/ - Look at Keycloak container logs:
Events Not Forwarding¶
- Verify the realm has the
keteevent listener enabled - Check destination container is reachable (network aliases)
- Enable debug logging in the provider
File Mounting Issues¶
- File not accessible in container: Ensure using
Transferable.of(bytes, 0777)with full permissions - Permission denied: Always use 0777 permissions parameter
- GitHub Actions failures: Never use
withFileSystemBind(), always useTransferable.of() - Missing permissions parameter: Verify all
Transferable.of()calls include0777as second parameter