diff --git a/application/application-api/src/main/java/io/github/bmarwell/keyserver/application/api/VerificationService.java b/application/application-api/src/main/java/io/github/bmarwell/keyserver/application/api/VerificationService.java
index ca09f19..3502055 100644
--- a/application/application-api/src/main/java/io/github/bmarwell/keyserver/application/api/VerificationService.java
+++ b/application/application-api/src/main/java/io/github/bmarwell/keyserver/application/api/VerificationService.java
@@ -5,9 +5,6 @@
*/
package io.github.bmarwell.keyserver.application.api;
-import io.github.bmarwell.keyserver.application.api.ex.TokenExpiredException;
-import io.github.bmarwell.keyserver.application.api.ex.TokenInvalidException;
-
/// Primary (inbound) port for synchronous UID verification.
///
/// Implementations handle the full token-to-publication flow synchronously,
diff --git a/ear/pom.xml b/ear/pom.xml
new file mode 100644
index 0000000..4fbe26a
--- /dev/null
+++ b/ear/pom.xml
@@ -0,0 +1,45 @@
+
+
When omitted, the shared PostgreSQL instance starts empty and the test class + * is responsible for creating its own data. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface DatabaseSeed { + + String[] value() default {}; + + String[] truncateAfter() default {}; +} diff --git a/integration-tests/helper/src/main/java/io/github/bmarwell/keyserver/it/extension/KeyserverAccess.java b/integration-tests/helper/src/main/java/io/github/bmarwell/keyserver/it/extension/KeyserverAccess.java new file mode 100644 index 0000000..b2d05da --- /dev/null +++ b/integration-tests/helper/src/main/java/io/github/bmarwell/keyserver/it/extension/KeyserverAccess.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2023-2026 The java-keyserver project team. + * + * SPDX-License-Identifier: EUPL-1.2 OR Apache-2.0 + */ +package io.github.bmarwell.keyserver.it.extension; + +import java.net.URI; + +public interface KeyserverAccess { + + URI pksBaseUri(); + + URI apiBaseUri(); + + String jdbcUrl(); + + String dbUser(); + + String dbPassword(); +} diff --git a/integration-tests/src/test/java/io/github/bmarwell/keyserver/it/extension/KeyserverContainerExtension.java b/integration-tests/helper/src/main/java/io/github/bmarwell/keyserver/it/extension/KeyserverContainerExtension.java similarity index 64% rename from integration-tests/src/test/java/io/github/bmarwell/keyserver/it/extension/KeyserverContainerExtension.java rename to integration-tests/helper/src/main/java/io/github/bmarwell/keyserver/it/extension/KeyserverContainerExtension.java index eada346..7915883 100644 --- a/integration-tests/src/test/java/io/github/bmarwell/keyserver/it/extension/KeyserverContainerExtension.java +++ b/integration-tests/helper/src/main/java/io/github/bmarwell/keyserver/it/extension/KeyserverContainerExtension.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; @@ -35,74 +36,31 @@ import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.images.builder.ImageFromDockerfile; -/** - * JUnit 5 extension that starts a shared PostgreSQL container and an Open Liberty container - * for the duration of the entire test session. - * - *
Containers are created once per JVM (stored at the root extension-context level) and - * shut down automatically when all tests finish because {@link ContainerHolder} implements - * {@link Store.CloseableResource}. - * - *
The extension implements {@link ParameterResolver}: any {@code @Test}, - * {@code @BeforeEach}, or {@code @BeforeAll} method that declares a {@link KeyserverAccess} - * (or {@link KeyserverInstance}) parameter will receive the shared running instance. - * - *
Parallel test classes: all classes in the same JVM share one set of containers. - * This avoids repeated Liberty startups (each takes ~2 minutes). Tests that require a clean - * database state should annotate their class with {@link DatabaseSeed}; the extension will - * execute the seed SQL before the class and truncate the declared tables afterwards. - * - *
This extension is normally activated via the {@link KeyserverIntegrationTest} meta-
- * annotation rather than being referenced directly.
- */
public class KeyserverContainerExtension implements BeforeAllCallback, AfterAllCallback, ParameterResolver {
private static final Logger LOG = LoggerFactory.getLogger(KeyserverContainerExtension.class);
- private static final Logger WEBSPHERE_LIBERTY_LOGGER = LoggerFactory.getLogger("websphere_liberty");
-
private static final Namespace NS = Namespace.create(KeyserverContainerExtension.class);
private static final String HOLDER_KEY = "containers";
- /** Liberty image pulled from IBM Container Registry. */
- private static final String LIBERTY_BASE_IMAGE =
- "icr.io/appcafe/open-liberty:kernel-slim-java25-openj9-ubi-minimal";
-
- /** Liberty's application server ready log message (CWWKF0011I). */
- private static final String LIBERTY_READY_LOG = ".*CWWKF0011I.*";
-
- /** Postgres network alias — matches KEYSERVER_DB_SERVER env var fed to Liberty. */
private static final String PG_NETWORK_ALIAS = "postgres";
-
private static final String PG_DATABASE = "keyserver";
private static final String PG_USER = "keyserver";
private static final String PG_PASSWORD = "keyserver";
- // -------------------------------------------------------------------------
- // JUnit 5 lifecycle callbacks
- // -------------------------------------------------------------------------
-
@Override
public void beforeAll(ExtensionContext context) {
- // Obtain or create the shared holder at the root context so it is shared
- // across all test classes in the JVM.
ContainerHolder holder = context.getRoot()
.getStore(NS)
.getOrComputeIfAbsent(HOLDER_KEY, _key -> startContainers(), ContainerHolder.class);
- // Apply @DatabaseSeed SQL if present on the test class.
findSeedAnnotation(context).ifPresent(seed -> applySeed(holder, seed));
}
@Override
public void afterAll(ExtensionContext context) {
- // Truncate tables declared in @DatabaseSeed so the next class starts clean.
findSeedAnnotation(context).ifPresent(seed -> truncateTables(requireHolder(context), seed));
}
- // -------------------------------------------------------------------------
- // ParameterResolver — injects KeyserverAccess / KeyserverInstance into tests
- // -------------------------------------------------------------------------
-
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
Class> type = parameterContext.getParameter().getType();
@@ -114,16 +72,10 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte
return requireHolder(extensionContext).toKeyserverInstance();
}
- // -------------------------------------------------------------------------
- // Container startup
- // -------------------------------------------------------------------------
-
- @SuppressWarnings("resource") // resources are closed via ContainerHolder.close()
+ @SuppressWarnings("resource")
private static ContainerHolder startContainers() {
LOG.info("Starting shared keyserver test containers...");
- // Network is tracked in ContainerHolder so it is closed in close() even if
- // Liberty startup throws after PostgreSQL has already started.
Network network = Network.newNetwork();
PostgreSQLContainer> postgres = null;
GenericContainer> liberty = null;
@@ -134,13 +86,11 @@ private static ContainerHolder startContainers() {
.withDatabaseName(PG_DATABASE)
.withUsername(PG_USER)
.withPassword(PG_PASSWORD);
-
postgres.start();
liberty = buildLibertyContainer(network);
liberty.start();
} catch (RuntimeException ex) {
- // Clean up anything that started before the failure.
if (liberty != null) {
liberty.stop();
}
@@ -168,37 +118,23 @@ private static ContainerHolder startContainers() {
}
private static GenericContainer> buildLibertyContainer(Network network) {
- String pksWarPath = requireSystemProperty("pks.war.path");
- String restWarPath = requireSystemProperty("rest.war.path");
- String serverXmlPath = requireSystemProperty("liberty.server.xml.path");
- String pgsqlJarPath = requireSystemProperty("pgsql.jar.path");
-
- /*
- * Build a custom Liberty image using ImageFromDockerfile so we can run
- * features.sh before copying the WARs (required for kernel-slim).
- *
- * Build order:
- * 1. Copy server.xml -> /config/server.xml
- * 2. Run features.sh -> installs only the features declared in server.xml
- * 3. Copy WARs -> /config/dropins/ (auto-deployed by Liberty)
- * 4. Copy JDBC driver -> /config/lib/global/postgresql.jar
- * 5. Run configure.sh -> final Liberty image prep step
- */
+ String deployablePath = requireSystemProperty("keyserver.deployable.path");
+ String serverXmlPath = requireSystemProperty("keyserver.server.xml.path");
+ String pgsqlJarPath = requireSystemProperty("keyserver.pgsql.jar.path");
+
+ String deployableName = Path.of(deployablePath).getFileName().toString();
String dockerfile = """
FROM %s
COPY --chown=1001:0 server.xml /config/server.xml
- RUN features.sh
- COPY --chown=1001:0 pks.war /config/dropins/pks.war
- COPY --chown=1001:0 rest.war /config/dropins/rest.war
+ COPY --chown=1001:0 deployable /config/dropins/%s
COPY --chown=1001:0 postgresql.jar /config/lib/global/postgresql.jar
RUN configure.sh
- """.formatted(LIBERTY_BASE_IMAGE);
+ """.formatted(KeyserverTestImage.LIBERTY_BASE_IMAGE, deployableName);
ImageFromDockerfile image = new ImageFromDockerfile("keyserver-liberty-it", true)
.withFileFromString("Dockerfile", dockerfile)
.withFileFromPath("server.xml", Path.of(serverXmlPath))
- .withFileFromPath("pks.war", Path.of(pksWarPath))
- .withFileFromPath("rest.war", Path.of(restWarPath))
+ .withFileFromPath("deployable", Path.of(deployablePath))
.withFileFromPath("postgresql.jar", Path.of(pgsqlJarPath));
return new GenericContainer<>(image)
@@ -208,14 +144,11 @@ private static GenericContainer> buildLibertyContainer(Network network) {
.withEnv("KEYSERVER_DB_NAME", PG_DATABASE)
.withEnv("KEYSERVER_DB_USER", PG_USER)
.withEnv("KEYSERVER_DB_PASSWORD", PG_PASSWORD)
- .waitingFor(Wait.forLogMessage(LIBERTY_READY_LOG, 1).withStartupTimeout(Duration.ofMinutes(3)))
- .withLogConsumer(new Slf4jLogConsumer(WEBSPHERE_LIBERTY_LOGGER));
+ .waitingFor(Wait.forLogMessage(KeyserverTestImage.LIBERTY_READY_LOG, 1)
+ .withStartupTimeout(Duration.ofMinutes(3)))
+ .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger("liberty")));
}
- // -------------------------------------------------------------------------
- // Database seeding helpers
- // -------------------------------------------------------------------------
-
private static void applySeed(ContainerHolder holder, DatabaseSeed seed) {
if (seed.value().length == 0) {
return;
@@ -259,16 +192,18 @@ private static String readClasspathResource(String resource) {
}
}
- // -------------------------------------------------------------------------
- // Internal helpers
- // -------------------------------------------------------------------------
-
private static Optional The {@link Network} is stored here so it can be closed after both
- * containers have stopped. With {@code ryuk.disabled=true} nothing else would
- * remove the dangling network from the Docker/Podman daemon.
- */
record ContainerHolder(
PostgreSQLContainer> postgres,
GenericContainer> liberty,
@@ -313,8 +236,6 @@ KeyserverInstance toKeyserverInstance() {
@Override
public void close() {
- // Best-effort shutdown: every resource is attempted even if a previous step throws.
- // All exceptions are collected; the first is rethrown with the rest as suppressed.
List The {@link KeyserverContainerExtension} reads this annotation in {@code beforeAll}.
- * SQL files listed in {@link #value()} are executed in order against the shared
- * PostgreSQL container before any test in the annotated class.
- *
- * After all tests in the class finish, the tables listed in {@link #truncateAfter()}
- * are truncated in a single statement (CASCADE) so the next test class starts clean.
- *
- * Not required: test classes that do not declare {@code @DatabaseSeed} are
- * perfectly valid. They use the shared PostgreSQL instance as-is. At the start of
- * a fresh test session the database is empty (schema only, created by Flyway migrations
- * on Liberty start-up). Tests without seeding must therefore be self-contained: either
- * they write their own data and assert with flexible matchers (e.g.
- * {@code isGreaterThanOrEqualTo(1)}), or they assert on data written earlier in the
- * same session. If strict isolation is needed, add {@code @DatabaseSeed} with an
- * empty {@link #value()} array to get the automatic truncation without any seed SQL.
- *
- * Example with seed SQL:
- * Example with truncation only (no seed SQL):
- * Implementations are injected by {@link KeyserverContainerExtension} as a
- * {@code ParameterResolver} argument into any {@code @Test}, {@code @BeforeEach},
- * or {@code @BeforeAll} method that declares a parameter of this type.
- *
- * Example:
- * The concrete type injected is {@link KeyserverInstance}. Test code should
- * declare the parameter as {@code KeyserverAccess} so tests remain decoupled from
- * the container implementation.
- *
- * Parallel test classes: all classes that use {@code @KeyserverIntegrationTest}
- * in the same JVM share the same running containers. Tests that need an isolated
- * database state should annotate their class with {@link DatabaseSeed}.
- */
-public interface KeyserverAccess {
-
- /** Base URI of the HKP (PKS) endpoint, e.g. {@code http://localhost:9080/pks}. */
- URI pksBaseUri();
-
- /** Base URI of the REST (JSON) endpoint, e.g. {@code http://localhost:9080/api}. */
- URI apiBaseUri();
-
- /** JDBC URL for direct database access during seed/verify steps. */
- String jdbcUrl();
-
- /** Database user. */
- String dbUser();
-
- /** Database password. */
- String dbPassword();
-}
diff --git a/integration-tests/src/test/java/io/github/bmarwell/keyserver/it/extension/KeyserverInstance.java b/integration-tests/src/test/java/io/github/bmarwell/keyserver/it/extension/KeyserverInstance.java
deleted file mode 100644
index 05765c7..0000000
--- a/integration-tests/src/test/java/io/github/bmarwell/keyserver/it/extension/KeyserverInstance.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright (C) 2023-2026 The java-keyserver project team.
- *
- * SPDX-License-Identifier: EUPL-1.2 OR Apache-2.0
- */
-package io.github.bmarwell.keyserver.it.extension;
-
-import java.net.URI;
-
-/**
- * Immutable snapshot of a running keyserver instance, injected by
- * {@link KeyserverContainerExtension} into test methods.
- *
- * All coordinates (URLs, JDBC URL, credentials) are resolved from the live containers
- * at injection time, so ports are always correct regardless of host mapping.
- *
- * Test code should declare parameters as {@link KeyserverAccess} rather than
- * {@code KeyserverInstance} for better decoupling from the container implementation.
- */
-public record KeyserverInstance(URI pksBaseUri, URI apiBaseUri, String jdbcUrl, String dbUser, String dbPassword)
- implements KeyserverAccess {}
diff --git a/integration-tests/src/test/java/io/github/bmarwell/keyserver/it/extension/KeyserverIntegrationTest.java b/integration-tests/src/test/java/io/github/bmarwell/keyserver/it/extension/KeyserverIntegrationTest.java
deleted file mode 100644
index bd4ba95..0000000
--- a/integration-tests/src/test/java/io/github/bmarwell/keyserver/it/extension/KeyserverIntegrationTest.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2023-2026 The java-keyserver project team.
- *
- * SPDX-License-Identifier: EUPL-1.2 OR Apache-2.0
- */
-package io.github.bmarwell.keyserver.it.extension;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-import org.junit.jupiter.api.Tag;
-import org.junit.jupiter.api.extension.ExtendWith;
-
-/**
- * Composed annotation for keyserver integration tests.
- *
- * Applying {@code @KeyserverIntegrationTest} to a test class is equivalent to:
- * The {@link KeyserverContainerExtension} starts a shared Open Liberty container and
- * a PostgreSQL container (once per JVM) and injects a {@link KeyserverAccess} argument
- * into any test method that declares one.
- *
- * Example:
- * For database seeding before a test class, additionally annotate with
- * {@link DatabaseSeed}.
- *
- * The {@code "integration"} tag lets CI selectively include or exclude these tests:
- * {@code
- * @DatabaseSeed(value = {"sql/some-keys.sql"}, truncateAfter = {"keys", "uids"})
- * class LookupIT {
- * // tests here can assume that some-keys.sql data is present
- * }
- * }
- *
- * {@code
- * @DatabaseSeed
- * class IsolatedWriteIT {
- * // starts each class with the default tables truncated
- * }
- * }
- */
-@Target(ElementType.TYPE)
-@Retention(RetentionPolicy.RUNTIME)
-@Inherited
-public @interface DatabaseSeed {
-
- /**
- * Classpath resources containing SQL INSERT/COPY statements to execute before
- * the test class runs. Paths are resolved relative to the classpath root.
- */
- String[] value() default {};
-
- /**
- * Table names to TRUNCATE CASCADE after all tests in the annotated class finish.
- * Defaults to the full set of application tables.
- */
- String[] truncateAfter() default {"uids", "keys", "verification_queue", "business_transactions"};
-}
diff --git a/integration-tests/src/test/java/io/github/bmarwell/keyserver/it/extension/KeyserverAccess.java b/integration-tests/src/test/java/io/github/bmarwell/keyserver/it/extension/KeyserverAccess.java
deleted file mode 100644
index 35b69ac..0000000
--- a/integration-tests/src/test/java/io/github/bmarwell/keyserver/it/extension/KeyserverAccess.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2023-2026 The java-keyserver project team.
- *
- * SPDX-License-Identifier: EUPL-1.2 OR Apache-2.0
- */
-package io.github.bmarwell.keyserver.it.extension;
-
-import java.net.URI;
-
-/**
- * Read-only view of a running keyserver test instance.
- *
- * {@code
- * @KeyserverIntegrationTest
- * class MyIT {
- *
- * @Test
- * void lookup(KeyserverAccess keyserver) throws Exception {
- * URI endpoint = keyserver.pksBaseUri().resolve("/pks/lookup?op=get&search=...");
- * // ...
- * }
- * }
- * }
- *
- * {@code
- * @Tag("integration")
- * @ExtendWith(KeyserverContainerExtension.class)
- * }
- *
- * {@code
- * @KeyserverIntegrationTest
- * class AddKeyIT {
- *
- * @Test
- * void add_key_returns_202(KeyserverAccess keyserver) throws Exception {
- * URI endpoint = keyserver.pksBaseUri().resolve("/pks/add");
- * // ...
- * }
- * }
- * }
- *
- * {@code
- * ./mvnw verify -pl integration-tests -am -P run-its -Dgroups=integration
- * }
- */
-@Target(ElementType.TYPE)
-@Retention(RetentionPolicy.RUNTIME)
-@Inherited
-@Tag("integration")
-@ExtendWith(KeyserverContainerExtension.class)
-public @interface KeyserverIntegrationTest {}
diff --git a/integration-tests/suite/pom.xml b/integration-tests/suite/pom.xml
new file mode 100644
index 0000000..eb9c1ce
--- /dev/null
+++ b/integration-tests/suite/pom.xml
@@ -0,0 +1,98 @@
+
+