This is an automated email from the ASF dual-hosted git repository. sergehuber pushed a commit to branch UNOMI-968-pluggable-persistence-itests in repository https://gitbox.apache.org/repos/asf/unomi.git
commit fcc8d2ef699b7a55340a66ce245bbcb9af52f4b8 Author: Serge Huber <[email protected]> AuthorDate: Mon Jul 20 23:25:44 2026 +0200 UNOMI-968: Pluggable persistence IT harness for ES, OS, and other backends Extract PersistenceITBackend SPI with capability-based skips so the same CorePersistenceITs/AllITs suite runs on Elasticsearch and OpenSearch as before, while external providers (PostgreSQL, etc.) can plug in via ServiceLoader without forking Unomi IT sources. --- itests/README.md | 84 +++++- itests/pom.xml | 32 +++ .../test/java/org/apache/unomi/itests/AllITs.java | 4 +- .../test/java/org/apache/unomi/itests/BaseIT.java | 309 +++++++++++---------- .../{AllITs.java => CorePersistenceITs.java} | 11 +- .../org/apache/unomi/itests/HealthCheckIT.java | 51 +++- .../java/org/apache/unomi/itests/JSONSchemaIT.java | 11 +- .../org/apache/unomi/itests/ProfileServiceIT.java | 6 +- .../itests/ProfileServiceWithoutOverwriteIT.java | 3 +- .../java/org/apache/unomi/itests/RolloverIT.java | 42 ++- .../migration/Migrate16xToCurrentVersionIT.java | 45 +-- .../itests/persistence/ElasticsearchITBackend.java | 117 ++++++++ .../itests/persistence/OpenSearchITBackend.java | 130 +++++++++ .../itests/persistence/PersistenceITBackend.java | 92 ++++++ .../persistence/PersistenceITBackendResolver.java | 110 ++++++++ .../persistence/PersistenceITCapabilities.java | 204 ++++++++++++++ .../unomi/itests/persistence/SearchBackendIT.java | 29 ++ ...e.unomi.itests.persistence.PersistenceITBackend | 2 + 18 files changed, 1076 insertions(+), 206 deletions(-) diff --git a/itests/README.md b/itests/README.md index ebb3d9708..20a5f79ed 100644 --- a/itests/README.md +++ b/itests/README.md @@ -32,7 +32,9 @@ and data migration. `docker-maven-plugin`). - **Maven Failsafe** runs a single entry point — `AllITs` — which aggregates all test classes. Each test class extends `BaseIT`, which handles Karaf startup, OSGi service - injection, and common test utilities. + injection, and common test utilities. Persistence is selected via + `unomi.persistence.provider` (Elasticsearch / OpenSearch today; see + [Pluggable persistence providers](#pluggable-persistence-providers)). A full IT run typically takes 20–30 minutes. The Karaf instance is created fresh for each run under `itests/target/exam/` with a UUID directory name. @@ -284,6 +286,86 @@ See the [Maven Failsafe plugin docs](https://maven.apache.org/surefire/maven-fai --- +## Pluggable persistence providers + +Unomi’s runtime persistence is an OSGi capability (`unomi.persistence;provider:=…`). +The IT harness follows the same idea: `BaseIT` resolves a test-only +`PersistenceITBackend` instead of hard-coding Elasticsearch / OpenSearch. + +### Provider selection + +| Property | Role | +|----------|------| +| `unomi.persistence.provider` | Preferred provider id (`elasticsearch`, `opensearch`, or an extension id) | +| `unomi.search.engine` | **Deprecated alias** — used when `unomi.persistence.provider` is unset | +| `unomi.persistence.it.backend` | Optional FQCN of a `PersistenceITBackend` implementation | + +Default CI cells set both `unomi.persistence.provider` and the deprecated +`unomi.search.engine` so existing `--use-opensearch` / scripts keep working. + +### Built-in backends + +| Id | Class | Suite for CI | +|----|-------|--------------| +| `elasticsearch` | `ElasticsearchITBackend` | `AllITs` (default) | +| `opensearch` | `OpenSearchITBackend` | `AllITs` with `-Duse.opensearch=true` | + +### Search-only vs core behavioural tests + +`AllITs` and `CorePersistenceITs` share the **same** test membership for maximum coverage. +Tests that need HTTP admin / snapshot / rollover APIs stay in the suite and use +`Assume` + `PersistenceITCapabilities` so unsupported backends **skip** (reported as +skipped, not green false-pass): + +| Class | Gate | +|-------|------| +| `Migrate16xToCurrentVersionIT` | `snapshotRestoreMigration()` | +| `RolloverIT` | `indexRolloverApi().isPresent()` + `httpAdminApi()`; switch on `LIFECYCLE` / `STATE_MANAGEMENT` | +| `HealthCheckIT` | Always asserts `karaf` / `unomi` / `persistence`; optional `providerNamedHealthProbe` / `clusterHealthProbe` | + +`HealthCheckIT` is not search-only: it runs on every provider. + +Elasticsearch / OpenSearch CI continues to use `AllITs`. Portable / non-search cells +(PostgreSQL, …) should run `CorePersistenceITs` (identical membership, clearer name). + +```bash +mvn clean install -P integration-tests -Dit.test=org.apache.unomi.itests.CorePersistenceITs +``` + +Capability flags gate remaining special-cases +(e.g. `snapshotRestoreMigration`, `flattenedRangeQueryResult`, `indexRolloverApi`) +so providers skip what they cannot support instead of checking product names. + +### Adding a third-party backend + +1. Implement `org.apache.unomi.itests.persistence.PersistenceITBackend` + (feature options, distribution name for `unomi:setup`, ConfigAdmin PID, + capabilities, await-ready / HTTP helpers as needed). +2. Put the implementation on the Failsafe test classpath. +3. Register it either: + - in `META-INF/services/org.apache.unomi.itests.persistence.PersistenceITBackend`, or + - via `-Dunomi.persistence.it.backend=com.example.MyPersistenceITBackend` +4. Select it with `-Dunomi.persistence.provider=<your-id>`. +5. Run `CorePersistenceITs` (same membership as `AllITs`). Incomplete SPI + implementations may still fail behavioural tests — unsupported HTTP-admin + features are skipped via `Assume`, not removed from the suite. + +`unomi-itests` publishes a **test-jar** (`mvn -pl itests install`) so external modules can +depend on `BaseIT`, `CorePersistenceITs`, and `PersistenceITBackend` without forking +sources. Optionally implement `prepareBeforeUnomiSetup` to patch ConfigAdmin (e.g. JDBC +DataSource) after `UnomiManagementService` is up and before `unomi:setup`. + +Example Failsafe exclude when running `AllITs` against a non-search provider: + +```xml +<excludedGroups>org.apache.unomi.itests.persistence.SearchBackendIT</excludedGroups> +``` + +(Note: JUnit 4 category filtering applies to classes Failsafe launches directly; +prefer `CorePersistenceITs` when using the suite entry point.) + +--- + ## Debugging Integration Tests ### Attaching a remote debugger to the test JVM diff --git a/itests/pom.xml b/itests/pom.xml index d28e1d2a6..026a1798f 100644 --- a/itests/pom.xml +++ b/itests/pom.xml @@ -28,6 +28,7 @@ <description>Apache Unomi Context Server integration tests</description> <properties> + <unomi.persistence.provider>elasticsearch</unomi.persistence.provider> <unomi.search.engine>elasticsearch</unomi.search.engine> <use.opensearch>false</use.opensearch> <docker.container.name>itests-opensearch</docker.container.name> @@ -277,6 +278,33 @@ </systemPropertyVariables> </configuration> </plugin> + <!-- Publish test classes so out-of-tree PersistenceService providers can implement + PersistenceITBackend and reuse CorePersistenceITs / BaseIT (UNOMI-968). --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <executions> + <execution> + <id>test-jar</id> + <goals> + <goal>test-jar</goal> + </goals> + <configuration> + <!-- Include IT classes and test resources (fixtures used by BaseIT.config). --> + <includes> + <include>org/apache/unomi/itests/**</include> + <include>**/*.cfg</include> + <include>**/*.csv</include> + <include>**/*.json</include> + <include>**/*.groovy</include> + <include>migration/**</include> + <include>schemas/**</include> + <include>META-INF/**</include> + </includes> + </configuration> + </execution> + </executions> + </plugin> </plugins> </build> @@ -306,6 +334,8 @@ </includes> <systemPropertyVariables> <my.system.property>foo</my.system.property> + <unomi.persistence.provider>elasticsearch</unomi.persistence.provider> + <!-- Deprecated alias; keep for older scripts --> <unomi.search.engine>elasticsearch</unomi.search.engine> <elasticsearch.port>${elasticsearch.port}</elasticsearch.port> <it.karaf.heap>${karaf.heap}</it.karaf.heap> @@ -432,6 +462,8 @@ </includes> <systemPropertyVariables> <my.system.property>foo</my.system.property> + <unomi.persistence.provider>opensearch</unomi.persistence.provider> + <!-- Deprecated alias; keep for older scripts --> <unomi.search.engine>opensearch</unomi.search.engine> <org.apache.unomi.opensearch.addresses>localhost:${opensearch.port}</org.apache.unomi.opensearch.addresses> <org.ops4j.pax.logging.DefaultServiceLog.level>INFO</org.ops4j.pax.logging.DefaultServiceLog.level> diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java b/itests/src/test/java/org/apache/unomi/itests/AllITs.java index 8617b1dcb..41351e5b0 100644 --- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java +++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java @@ -24,7 +24,9 @@ import org.junit.runner.RunWith; import org.junit.runners.Suite.SuiteClasses; /** - * Defines suite of test classes to run. + * Defines suite of test classes to run (Elasticsearch / OpenSearch CI default). + * Same membership as {@link CorePersistenceITs}; use capabilities + {@code Assume} for + * provider-specific skips rather than maintaining a smaller suite. * * @author Sergiy Shyrkov */ diff --git a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java index 8587f4f8a..5ec6385ba 100644 --- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java @@ -22,8 +22,6 @@ import org.apache.camel.CamelContext; import org.apache.camel.Route; import org.apache.camel.ServiceStatus; import org.apache.commons.io.IOUtils; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.HttpEntity; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.*; @@ -54,6 +52,9 @@ import org.apache.unomi.api.tenants.Tenant; import org.apache.unomi.api.tenants.TenantService; import org.apache.unomi.api.utils.ConditionBuilder; import org.apache.unomi.groovy.actions.services.GroovyActionsService; +import org.apache.unomi.itests.persistence.PersistenceITBackend; +import org.apache.unomi.itests.persistence.PersistenceITBackendResolver; +import org.apache.unomi.itests.persistence.PersistenceITCapabilities; import org.apache.unomi.itests.tools.LogChecker; import org.apache.unomi.itests.tools.httpclient.HttpClientThatWaitsForUnomi; import org.apache.unomi.lifecycle.BundleWatcher; @@ -98,9 +99,11 @@ import javax.net.ssl.X509TrustManager; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -135,16 +138,23 @@ public abstract class BaseIT extends KarafTestSupport { protected static final int DEFAULT_TRYING_TRIES = 10; protected static final int DEFAULT_SHOULDBETRUE_TRIES = 5; - protected static final String SEARCH_ENGINE_PROPERTY = "unomi.search.engine"; + /** @deprecated use {@link PersistenceITBackendResolver#PROVIDER_PROPERTY}. */ + @Deprecated + protected static final String SEARCH_ENGINE_PROPERTY = PersistenceITBackendResolver.SEARCH_ENGINE_PROPERTY; protected static final String SEARCH_ENGINE_HTTPREQUEST_LOG_LEVEL = "unomi.search.engine.httprequest.log.level"; - protected static final String SEARCH_ENGINE_ELASTICSEARCH = "elasticsearch"; - protected static final String SEARCH_ENGINE_OPENSEARCH = "opensearch"; + protected static final String SEARCH_ENGINE_ELASTICSEARCH = PersistenceITBackendResolver.PROVIDER_ELASTICSEARCH; + protected static final String SEARCH_ENGINE_OPENSEARCH = PersistenceITBackendResolver.PROVIDER_OPENSEARCH; + protected static final String PERSISTENCE_PROVIDER_PROPERTY = PersistenceITBackendResolver.PROVIDER_PROPERTY; protected static final String RESOLVER_DEBUG_PROPERTY = "it.unomi.resolver.debug"; protected static final String ENABLE_LOG_CHECKING_PROPERTY = "it.unomi.log.checking.enabled"; protected static final String CAMEL_DEBUG_PROPERTY = "it.unomi.camel.debug"; protected static boolean unomiStarted = false; + /** + * Active provider id. Kept for existing tests; prefer {@link #getPersistenceBackend()}. + */ protected static String searchEngine = SEARCH_ENGINE_ELASTICSEARCH; + private static PersistenceITBackend persistenceBackend; private static boolean searchEngineConfiguredForTesting = false; private static boolean searchEngineHealthVerifiedAfterStartup = false; @@ -239,37 +249,71 @@ public abstract class BaseIT extends KarafTestSupport { } protected void checkSearchEngine() { - searchEngine = System.getProperty(SEARCH_ENGINE_PROPERTY, SEARCH_ENGINE_ELASTICSEARCH); + resolvePersistenceBackend(); configureSearchEngineForTesting(); } + /** + * Resolves (and caches) the active {@link PersistenceITBackend} from system properties / + * ServiceLoader. Also updates the legacy {@link #searchEngine} field. + */ + protected static PersistenceITBackend resolvePersistenceBackend() { + if (persistenceBackend == null) { + persistenceBackend = PersistenceITBackendResolver.resolve(); + searchEngine = persistenceBackend.providerId(); + } + return persistenceBackend; + } + + protected static PersistenceITBackend getPersistenceBackend() { + return resolvePersistenceBackend(); + } + + protected PersistenceITCapabilities persistenceCapabilities() { + return getPersistenceBackend().capabilities(); + } + + /** ConfigAdmin PID for the active persistence provider (e.g. throwExceptions). */ + protected String persistenceConfigPid() { + return getPersistenceBackend().persistenceConfigPid(); + } + @Before public void waitForStartup() throws InterruptedException { // disable retry retry = new KarafTestSupport.Retry(false); - // Check search engine and apply any necessary fixes (e.g., default_template deletion) + // Resolve provider and apply any necessary backend prep (e.g., zero-replica template) checkSearchEngine(); + try { + getPersistenceBackend().awaitBackendReady(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw e; + } catch (Exception e) { + throw new IllegalStateException("Persistence backend awaitBackendReady failed", e); + } // Start Unomi if not already done if (!unomiStarted) { // We must check that the Unomi Management Service is up and running before launching the // command otherwise the start configuration will not be properly populated. waitForUnomiManagementService(); - if (SEARCH_ENGINE_ELASTICSEARCH.equals(searchEngine)) { - LOGGER.info("Starting Unomi with elasticsearch search engine..."); - System.out.println("==== Starting Unomi with elasticsearch search engine..."); - executeCommand("unomi:setup -d=unomi-distribution-elasticsearch -f=true"); - executeCommand("unomi:start"); - } else if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)){ - LOGGER.info("Starting Unomi with opensearch search engine..."); - System.out.println("==== Starting Unomi with opensearch search engine..."); - executeCommand("unomi:setup -d=unomi-distribution-opensearch -f=true"); - executeCommand("unomi:start"); - } else { - LOGGER.error("Unknown search engine: " + searchEngine); - throw new InterruptedException("Unknown search engine: " + searchEngine); + PersistenceITBackend backend = getPersistenceBackend(); + try { + backend.prepareBeforeUnomiSetup(bundleContext, configurationAdmin); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw e; + } catch (Exception e) { + throw new IllegalStateException("Persistence backend prepareBeforeUnomiSetup failed", e); } + String distribution = backend.distributionFeature(); + LOGGER.info("Starting Unomi with persistence provider {} (distribution {})...", backend.providerId(), distribution); + System.out.println("==== Starting Unomi with persistence provider " + backend.providerId() + + " (distribution " + distribution + ")..."); + executeCommand("unomi:setup -d=" + distribution + " -f=true"); + executeCommand("unomi:start"); unomiStarted = true; } @@ -542,6 +586,43 @@ public abstract class BaseIT extends KarafTestSupport { Thread.sleep(1000); } + /** + * Resolves a fixture under {@code src/test/resources} for Pax Exam {@code replaceConfigurationFile}. + * Prefers the local filesystem (Unomi itests module); falls back to the classpath so out-of-tree + * consumers of the {@code unomi-itests} test-jar can reuse {@link #config()} without copying files. + */ + protected static File resolveTestResource(String pathUnderSrcTestResources) { + File local = new File(pathUnderSrcTestResources); + if (local.isFile()) { + return local; + } + String classpathName = pathUnderSrcTestResources; + final String prefix = "src/test/resources/"; + if (classpathName.startsWith(prefix)) { + classpathName = classpathName.substring(prefix.length()); + } + URL url = BaseIT.class.getClassLoader().getResource(classpathName); + if (url == null) { + throw new IllegalStateException("Missing test resource: " + pathUnderSrcTestResources + + " (not on filesystem or classpath)"); + } + try { + if ("file".equals(url.getProtocol())) { + return new File(url.toURI()); + } + String fileName = new File(classpathName).getName(); + Path tmp = Files.createTempFile("unomi-it-resource-", "-" + fileName); + try (InputStream in = url.openStream()) { + Files.copy(in, tmp, StandardCopyOption.REPLACE_EXISTING); + } + File out = tmp.toFile(); + out.deleteOnExit(); + return out; + } catch (Exception e) { + throw new IllegalStateException("Cannot resolve test resource " + pathUnderSrcTestResources, e); + } + } + @Override public MavenArtifactUrlReference getKarafDistribution() { return maven().groupId("org.apache.unomi").artifactId("unomi").versionAsInProject().type("tar.gz"); @@ -554,120 +635,41 @@ public abstract class BaseIT extends KarafTestSupport { System.out.println("==== Configuring container"); } - searchEngine = System.getProperty(SEARCH_ENGINE_PROPERTY, SEARCH_ENGINE_ELASTICSEARCH); + resolvePersistenceBackend(); if (!configLogged) { - LOGGER.info("Search Engine: {}", searchEngine); - System.out.println("Search Engine: " + searchEngine); - } - - // Define features option based on search engine - Option featuresOption; - Option distributionOption; - if (SEARCH_ENGINE_ELASTICSEARCH.equals(searchEngine)) { - featuresOption = features( - maven().groupId("org.apache.unomi").artifactId("unomi-kar").versionAsInProject().type("xml").classifier("features"), - "unomi-base", - "unomi-startup", - "unomi-elasticsearch-core", - "unomi-persistence-core", - "unomi-services", - "unomi-cxs-privacy-extension-services", - "unomi-plugins-base", - "unomi-plugins-request", - "unomi-plugins-mail", - "unomi-plugins-optimization-test", - "unomi-rest-api", - "unomi-cxs-privacy-extension", - "unomi-elasticsearch-conditions", - "unomi-cxs-lists-extension", - "unomi-cxs-geonames-extension", - "unomi-shell-dev-commands", - "unomi-wab", - "unomi-web-tracker", - "unomi-healthcheck-elasticsearch", - "unomi-router-karaf-feature", - "unomi-groovy-actions", - "unomi-rest-ui", - "cdp-graphql-feature", - "unomi-startup-complete" - ); - distributionOption = features( - maven().groupId("org.apache.unomi").artifactId("unomi-distribution").versionAsInProject().type("xml").classifier("features"), - "unomi-distribution-elasticsearch-graphql" - ); - } else if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) { - featuresOption = features( - maven().groupId("org.apache.unomi").artifactId("unomi-kar").versionAsInProject().type("xml").classifier("features"), - "unomi-base", - "unomi-startup", - "unomi-opensearch-core", - "unomi-persistence-core", - "unomi-services", - "unomi-cxs-privacy-extension-services", - "unomi-plugins-base", - "unomi-plugins-request", - "unomi-plugins-mail", - "unomi-plugins-optimization-test", - "unomi-rest-api", - "unomi-cxs-privacy-extension", - "unomi-opensearch-conditions", - "unomi-cxs-lists-extension", - "unomi-cxs-geonames-extension", - "unomi-shell-dev-commands", - "unomi-wab", - "unomi-web-tracker", - "unomi-healthcheck-opensearch", - "unomi-router-karaf-feature", - "unomi-groovy-actions", - "unomi-rest-ui", - "cdp-graphql-feature", - "unomi-startup-complete" - ); - distributionOption = features( - maven().groupId("org.apache.unomi").artifactId("unomi-distribution").versionAsInProject().type("xml").classifier("features"), - "unomi-distribution-opensearch-graphql" - ); - } else { - throw new IllegalArgumentException("Unknown search engine: " + searchEngine); + LOGGER.info("Persistence provider: {}", searchEngine); + System.out.println("Persistence provider: " + searchEngine); } + PersistenceITBackend backend = getPersistenceBackend(); + Option[] backendFeatures = backend.featureOptions(); + Option[] backendConfig = backend.configurationOptions(); + Option[] options = new Option[]{ - replaceConfigurationFile("etc/org.apache.unomi.router.cfg", new File("src/test/resources/org.apache.unomi.router.cfg")), - replaceConfigurationFile("data/tmp/1-basic-test.csv", new File("src/test/resources/1-basic-test.csv")), - replaceConfigurationFile("data/tmp/recurrent_import/2-surfers-test.csv", new File("src/test/resources/2-surfers-test.csv")), - replaceConfigurationFile("data/tmp/recurrent_import/3-surfers-overwrite-test.csv", new File("src/test/resources/3-surfers-overwrite-test.csv")), - replaceConfigurationFile("data/tmp/recurrent_import/4-surfers-delete-test.csv", new File("src/test/resources/4-surfers-delete-test.csv")), - replaceConfigurationFile("data/tmp/recurrent_import/5-ranking-test.csv", new File("src/test/resources/5-ranking-test.csv")), - replaceConfigurationFile("data/tmp/recurrent_import/6-actors-test.csv", new File("src/test/resources/6-actors-test.csv")), - replaceConfigurationFile("data/tmp/testLogin.json", new File("src/test/resources/testLogin.json")), - replaceConfigurationFile("data/tmp/testCopyProperties.json", new File("src/test/resources/testCopyProperties.json")), - replaceConfigurationFile("data/tmp/testCopyPropertiesWithoutSystemTags.json", new File("src/test/resources/testCopyPropertiesWithoutSystemTags.json")), - replaceConfigurationFile("data/tmp/testLoginEventCondition.json", new File("src/test/resources/testLoginEventCondition.json")), - replaceConfigurationFile("data/tmp/testClickEventCondition.json", new File("src/test/resources/testClickEventCondition.json")), - replaceConfigurationFile("data/tmp/testRuleGroovyAction.json", new File("src/test/resources/testRuleGroovyAction.json")), - replaceConfigurationFile("data/tmp/conditions/testIdsConditionLegacy.json", new File("src/test/resources/conditions/testIdsConditionLegacy.json")), - replaceConfigurationFile("data/tmp/conditions/testIdsConditionNew.json", new File("src/test/resources/conditions/testIdsConditionNew.json")), - replaceConfigurationFile("data/tmp/conditions/testBooleanConditionLegacy.json", new File("src/test/resources/conditions/testBooleanConditionLegacy.json")), - replaceConfigurationFile("data/tmp/conditions/testPropertyConditionLegacy.json", new File("src/test/resources/conditions/testPropertyConditionLegacy.json")), - replaceConfigurationFile("data/tmp/groovy/UpdateAddressAction.groovy", new File("src/test/resources/groovy/UpdateAddressAction.groovy")), + replaceConfigurationFile("etc/org.apache.unomi.router.cfg", resolveTestResource("src/test/resources/org.apache.unomi.router.cfg")), + replaceConfigurationFile("data/tmp/1-basic-test.csv", resolveTestResource("src/test/resources/1-basic-test.csv")), + replaceConfigurationFile("data/tmp/recurrent_import/2-surfers-test.csv", resolveTestResource("src/test/resources/2-surfers-test.csv")), + replaceConfigurationFile("data/tmp/recurrent_import/3-surfers-overwrite-test.csv", resolveTestResource("src/test/resources/3-surfers-overwrite-test.csv")), + replaceConfigurationFile("data/tmp/recurrent_import/4-surfers-delete-test.csv", resolveTestResource("src/test/resources/4-surfers-delete-test.csv")), + replaceConfigurationFile("data/tmp/recurrent_import/5-ranking-test.csv", resolveTestResource("src/test/resources/5-ranking-test.csv")), + replaceConfigurationFile("data/tmp/recurrent_import/6-actors-test.csv", resolveTestResource("src/test/resources/6-actors-test.csv")), + replaceConfigurationFile("data/tmp/testLogin.json", resolveTestResource("src/test/resources/testLogin.json")), + replaceConfigurationFile("data/tmp/testCopyProperties.json", resolveTestResource("src/test/resources/testCopyProperties.json")), + replaceConfigurationFile("data/tmp/testCopyPropertiesWithoutSystemTags.json", resolveTestResource("src/test/resources/testCopyPropertiesWithoutSystemTags.json")), + replaceConfigurationFile("data/tmp/testLoginEventCondition.json", resolveTestResource("src/test/resources/testLoginEventCondition.json")), + replaceConfigurationFile("data/tmp/testClickEventCondition.json", resolveTestResource("src/test/resources/testClickEventCondition.json")), + replaceConfigurationFile("data/tmp/testRuleGroovyAction.json", resolveTestResource("src/test/resources/testRuleGroovyAction.json")), + replaceConfigurationFile("data/tmp/conditions/testIdsConditionLegacy.json", resolveTestResource("src/test/resources/conditions/testIdsConditionLegacy.json")), + replaceConfigurationFile("data/tmp/conditions/testIdsConditionNew.json", resolveTestResource("src/test/resources/conditions/testIdsConditionNew.json")), + replaceConfigurationFile("data/tmp/conditions/testBooleanConditionLegacy.json", resolveTestResource("src/test/resources/conditions/testBooleanConditionLegacy.json")), + replaceConfigurationFile("data/tmp/conditions/testPropertyConditionLegacy.json", resolveTestResource("src/test/resources/conditions/testPropertyConditionLegacy.json")), + replaceConfigurationFile("data/tmp/groovy/UpdateAddressAction.groovy", resolveTestResource("src/test/resources/groovy/UpdateAddressAction.groovy")), editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg", "log4j2.rootLogger.level", "INFO"), editConfigurationFilePut("etc/org.apache.karaf.features.cfg", "serviceRequirements", "disable"), editConfigurationFilePut("etc/system.properties", "my.system.property", System.getProperty("my.system.property")), - editConfigurationFilePut("etc/system.properties", SEARCH_ENGINE_PROPERTY, System.getProperty(SEARCH_ENGINE_PROPERTY, SEARCH_ENGINE_ELASTICSEARCH)), - editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.cluster.name", "contextElasticSearchITests"), - editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.addresses", "localhost:" + getSearchPort()), - editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.taskWaitingPollingInterval", "50"), - editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.rollover.maxDocs", "300"), - editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.minimalClusterState", "YELLOW"), - editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.cluster.name", "contextElasticSearchITests"), - editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.addresses", "localhost:" + getSearchPort()), - editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.username", "admin"), - editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.password", "Unomi.1ntegrat10n.Tests"), - editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.sslEnable", "false"), - editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.sslTrustAllCertificates", "true"), - editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.rollover.maxDocs", "300"), - editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.minimalClusterState", "YELLOW"), + editConfigurationFilePut("etc/system.properties", SEARCH_ENGINE_PROPERTY, searchEngine), + editConfigurationFilePut("etc/system.properties", PERSISTENCE_PROVIDER_PROPERTY, searchEngine), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.migration.tenant.id", TEST_TENANT_ID), // Default scheduler.thread.poolSize (5) is sized for the near-instant in-memory unit-test // double, not a real ES/OS backend. Under real refresh/write latency, the checker, task @@ -680,9 +682,6 @@ public abstract class BaseIT extends KarafTestSupport { systemProperty("org.ops4j.pax.exam.rbc.rmi.port").value("1199"), systemProperty("org.apache.unomi.healthcheck.enabled").value("true"), - featuresOption, // Add the features option - distributionOption, // Add the distribution option - configureConsole().startRemoteShell(), logLevel(LogLevel.INFO), keepRuntimeFolder(), @@ -691,6 +690,8 @@ public abstract class BaseIT extends KarafTestSupport { }; List<Option> karafOptions = new ArrayList<>(); karafOptions.addAll(Arrays.asList(options)); + karafOptions.addAll(Arrays.asList(backendConfig)); + karafOptions.addAll(Arrays.asList(backendFeatures)); String karafDebug = System.getProperty("it.karaf.debug"); if (karafDebug != null) { @@ -1435,6 +1436,10 @@ public abstract class BaseIT extends KarafTestSupport { if (searchEngineConfiguredForTesting) { return; } + if (!persistenceCapabilities().httpAdminApi()) { + searchEngineConfiguredForTesting = true; + return; + } try (CloseableHttpClient client = createSearchEngineHttpClient()) { String baseUrl = getSearchEngineBaseUrl(); ensureZeroReplicaIndexTemplate(client, baseUrl); @@ -1464,6 +1469,9 @@ public abstract class BaseIT extends KarafTestSupport { } private void enforceZeroReplicasAndWaitForCluster(String context) { + if (!persistenceCapabilities().httpAdminApi()) { + return; + } try (CloseableHttpClient client = createSearchEngineHttpClient()) { String baseUrl = getSearchEngineBaseUrl(); ensureZeroReplicaIndexTemplate(client, baseUrl); @@ -1517,6 +1525,14 @@ public abstract class BaseIT extends KarafTestSupport { * Stage 3 (post-Unomi start): assert cluster health for single-node IT (ES and OpenSearch). */ protected void assertClusterHealthy(String context) { + if (!persistenceCapabilities().httpAdminApi()) { + try { + getPersistenceBackend().assertHealthyAfterUnomiStart(); + } catch (Exception e) { + throw new IllegalStateException("Backend health assertion failed: " + context, e); + } + return; + } try (CloseableHttpClient client = createSearchEngineHttpClient()) { String baseUrl = getSearchEngineBaseUrl(); ensureZeroReplicaIndexTemplate(client, baseUrl); @@ -1550,36 +1566,31 @@ public abstract class BaseIT extends KarafTestSupport { } protected static String getSearchEngineBaseUrl() { - if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) { - return "http://localhost:" + getSearchPort(); - } - return "http://localhost:" + getSearchPort(); + requireHttpAdminApi("getSearchEngineBaseUrl"); + return getPersistenceBackend().searchBaseUrl(); } protected CloseableHttpClient createSearchEngineHttpClient() throws IOException { - if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) { - BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); - credentialsProvider.setCredentials(AuthScope.ANY, - new UsernamePasswordCredentials("admin", "Unomi.1ntegrat10n.Tests")); - return HttpUtils.initHttpClient(true, credentialsProvider); - } - return HttpUtils.initHttpClient(true, null); + requireHttpAdminApi("createSearchEngineHttpClient"); + return getPersistenceBackend().createSearchHttpClient(); } /** - * Gets the appropriate search engine port based on the configured search engine. + * Gets the appropriate search engine port based on the configured persistence provider. * * @return The port number as a string */ protected static String getSearchPort() { - String searchEngine = System.getProperty(SEARCH_ENGINE_PROPERTY, SEARCH_ENGINE_ELASTICSEARCH); - if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) { - // For OpenSearch, get the port from the system property set by maven-failsafe-plugin - return System.getProperty("org.apache.unomi.opensearch.addresses", "localhost:9401") - .split(":")[1]; // Extract port number from "localhost:9401" - } else { - // For Elasticsearch, use the default port or system property if set - return System.getProperty("elasticsearch.port", "9400"); + requireHttpAdminApi("getSearchPort"); + return getPersistenceBackend().searchPort(); + } + + private static void requireHttpAdminApi(String caller) { + PersistenceITBackend backend = getPersistenceBackend(); + if (!backend.capabilities().httpAdminApi()) { + throw new UnsupportedOperationException( + caller + " requires httpAdminApi, but provider '" + backend.providerId() + + "' does not advertise it (skip via Assume / SearchBackendIT / CorePersistenceITs)"); } } diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java b/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java similarity index 85% copy from itests/src/test/java/org/apache/unomi/itests/AllITs.java copy to itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java index 8617b1dcb..6cc692a0d 100644 --- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java +++ b/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java @@ -24,9 +24,12 @@ import org.junit.runner.RunWith; import org.junit.runners.Suite.SuiteClasses; /** - * Defines suite of test classes to run. - * - * @author Sergiy Shyrkov + * Full behavioural IT suite for any {@code PersistenceService} provider. + * <p> + * Membership matches {@link AllITs}. Tests that need HTTP admin / snapshot / rollover + * APIs use {@link org.junit.Assume} on {@link org.apache.unomi.itests.persistence.PersistenceITCapabilities} + * so unsupported backends <em>skip</em> (not fail, not false-pass). Prefer this suite for + * PostgreSQL, in-memory, JDBC, etc.; Elasticsearch / OpenSearch CI may keep using {@link AllITs}. */ @RunWith(ProgressSuite.class) @SuiteClasses({ @@ -83,5 +86,5 @@ import org.junit.runners.Suite.SuiteClasses; RolloverIT.class, HealthCheckIT.class }) -public class AllITs { +public class CorePersistenceITs { } diff --git a/itests/src/test/java/org/apache/unomi/itests/HealthCheckIT.java b/itests/src/test/java/org/apache/unomi/itests/HealthCheckIT.java index 9fe11829d..305c2f38e 100644 --- a/itests/src/test/java/org/apache/unomi/itests/HealthCheckIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/HealthCheckIT.java @@ -41,7 +41,12 @@ import java.util.concurrent.*; import static org.junit.Assert.fail; /** - * Health Check Integration Tests + * Health Check Integration Tests. + * <p> + * Always asserts {@code karaf}, {@code unomi}, and {@code persistence} LIVE. + * Optional probes ({@code providerNamedHealthProbe}, {@code clusterHealthProbe}) are + * asserted only when the active {@link org.apache.unomi.itests.persistence.PersistenceITCapabilities} + * advertise them — so the same class covers Elasticsearch, OpenSearch, PostgreSQL, etc. */ @RunWith(PaxExam.class) @ExamReactorStrategy(PerSuite.class) @@ -57,15 +62,10 @@ public class HealthCheckIT extends BaseIT { public void testHealthCheck() { try { List<HealthCheckResponse> response = get(HEALTHCHECK_ENDPOINT, new TypeReference<>() {}); - LOGGER.info("configured search engine: {}", searchEngine); + LOGGER.info("configured persistence provider: {}", getPersistenceBackend().providerId()); LOGGER.info("health check response: {}", response); Assert.assertNotNull(response); - Assert.assertEquals(5, response.size()); - Assert.assertTrue(response.stream().anyMatch(r -> r.getName().equals("karaf") && r.getStatus() == HealthCheckResponse.Status.LIVE)); - Assert.assertTrue(response.stream().anyMatch(r -> r.getName().equals(searchEngine) && r.getStatus() == HealthCheckResponse.Status.LIVE)); - Assert.assertTrue(response.stream().anyMatch(r -> r.getName().equals("unomi") && r.getStatus() == HealthCheckResponse.Status.LIVE)); - Assert.assertTrue(response.stream().anyMatch(r -> r.getName().equals("persistence") && r.getStatus() == HealthCheckResponse.Status.LIVE)); - Assert.assertTrue(response.stream().anyMatch(r -> r.getName().equals("cluster") && r.getStatus() == HealthCheckResponse.Status.LIVE)); + assertHealthCheckLive(response); } catch (Exception e) { LOGGER.error("Error while executing health check", e); fail("Error while executing health check" + e.getMessage()); @@ -88,12 +88,7 @@ public class HealthCheckIT extends BaseIT { } for (Future<List<HealthCheckResponse>> future : futures) { List<HealthCheckResponse> health = future.get(10, TimeUnit.SECONDS); - Assert.assertEquals(5, health.size()); - Assert.assertTrue(health.stream().anyMatch(r -> r.getName().equals("karaf") && r.getStatus() == HealthCheckResponse.Status.LIVE)); - Assert.assertTrue(health.stream().anyMatch(r -> r.getName().equals(searchEngine) && r.getStatus() == HealthCheckResponse.Status.LIVE)); - Assert.assertTrue(health.stream().anyMatch(r -> r.getName().equals("unomi") && r.getStatus() == HealthCheckResponse.Status.LIVE)); - Assert.assertTrue(health.stream().anyMatch(r -> r.getName().equals("persistence") && r.getStatus() == HealthCheckResponse.Status.LIVE)); - Assert.assertTrue(health.stream().anyMatch(r -> r.getName().equals("cluster") && r.getStatus() == HealthCheckResponse.Status.LIVE)); + assertHealthCheckLive(health); } Thread.sleep(10); } @@ -109,6 +104,34 @@ public class HealthCheckIT extends BaseIT { } } + private void assertHealthCheckLive(List<HealthCheckResponse> response) { + Assert.assertNotNull(response); + Assert.assertTrue(response.stream().anyMatch(r -> r.getName().equals("karaf") && r.getStatus() == HealthCheckResponse.Status.LIVE)); + Assert.assertTrue(response.stream().anyMatch(r -> r.getName().equals("unomi") && r.getStatus() == HealthCheckResponse.Status.LIVE)); + Assert.assertTrue(response.stream().anyMatch(r -> r.getName().equals("persistence") && r.getStatus() == HealthCheckResponse.Status.LIVE)); + int expectedMinProbes = 3; // karaf, unomi, persistence + boolean named = persistenceCapabilities().providerNamedHealthProbe(); + boolean cluster = persistenceCapabilities().clusterHealthProbe(); + if (named) { + expectedMinProbes++; + String namedProbe = getPersistenceBackend().providerId(); + Assert.assertTrue(response.stream().anyMatch(r -> r.getName().equals(namedProbe) && r.getStatus() == HealthCheckResponse.Status.LIVE)); + } + if (cluster) { + expectedMinProbes++; + Assert.assertTrue(response.stream().anyMatch(r -> r.getName().equals("cluster") && r.getStatus() == HealthCheckResponse.Status.LIVE)); + } + // ES/OS advertise both optional probes → keep historical exact size (5). + // Other backends may expose extra probes; require at least the expected set. + if (named && cluster) { + Assert.assertEquals("Unexpected health probe count: " + response, expectedMinProbes, response.size()); + } else { + Assert.assertTrue( + "Expected at least " + expectedMinProbes + " health probes, got " + response.size() + ": " + response, + response.size() >= expectedMinProbes); + } + } + protected <T> T get(final String url, TypeReference<T> typeReference) { CloseableHttpResponse response = null; try { diff --git a/itests/src/test/java/org/apache/unomi/itests/JSONSchemaIT.java b/itests/src/test/java/org/apache/unomi/itests/JSONSchemaIT.java index e37c23cc8..e9fcd7c37 100644 --- a/itests/src/test/java/org/apache/unomi/itests/JSONSchemaIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/JSONSchemaIT.java @@ -25,6 +25,7 @@ import org.apache.http.util.EntityUtils; import org.apache.unomi.api.Event; import org.apache.unomi.api.Scope; import org.apache.unomi.api.conditions.Condition; +import org.apache.unomi.itests.persistence.PersistenceITCapabilities; import org.apache.unomi.itests.tools.LogChecker; import org.apache.unomi.itests.tools.httpclient.HttpClientThatWaitsForUnomi; import org.apache.unomi.schema.api.JsonSchemaWrapper; @@ -377,16 +378,14 @@ public class JSONSchemaIT extends BaseIT { // Refresh to ensure event is queryable refreshPersistence(Event.class); final Condition finalCondition = condition; - // For Elasticsearch, range queries on flattened properties should return null or empty list - // For OpenSearch, they may return results + // Providers differ on range queries against flattened properties (hits vs empty). // We just need to wait for the query to execute (not throw an exception) refreshPersistence(Event.class); org.apache.unomi.api.PartialList<Event> queryResult = persistenceService.query(finalCondition, null, Event.class, 0, -1); - if ("opensearch".equals(searchEngine)) { - assertNotNull("OpenSearch should return results for flattened properties", queryResult); + if (persistenceCapabilities().flattenedRangeQueryResult() == PersistenceITCapabilities.FlattenedRangeQueryResult.HITS) { + assertNotNull("provider that returns flattened range hits should return results", queryResult); } else { - // Elasticsearch should return null or empty list for range queries on flattened properties - assertTrue("Elasticsearch should return null or empty list for flattened properties range query", + assertTrue("provider that returns empty for flattened range should return null or empty list", queryResult == null || queryResult.getList() == null || queryResult.getList().isEmpty()); } diff --git a/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java b/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java index dff72a1dc..e256c7004 100644 --- a/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java @@ -153,7 +153,7 @@ public class ProfileServiceIT extends BaseIT { @Test public void testGetProfileWithWrongScrollerIdThrowException() throws InterruptedException, IOException { boolean throwExceptionCurrent = false; - Configuration searchEngineConfiguration = configurationAdmin.getConfiguration("org.apache.unomi.persistence." + searchEngine); + Configuration searchEngineConfiguration = configurationAdmin.getConfiguration(persistenceConfigPid()); if (searchEngineConfiguration != null && searchEngineConfiguration.getProperties().get("throwExceptions") != null) { try { if (searchEngineConfiguration.getProperties().get("throwExceptions") instanceof String) { @@ -167,7 +167,7 @@ public class ProfileServiceIT extends BaseIT { } } - updateConfiguration(null, "org.apache.unomi.persistence." + searchEngine, "throwExceptions", true); + updateConfiguration(null, persistenceConfigPid(), "throwExceptions", true); Query query = new Query(); query.setLimit(2); @@ -180,7 +180,7 @@ public class ProfileServiceIT extends BaseIT { } catch (RuntimeException ex) { // Should get here since this scenario should throw exception } finally { - updateConfiguration(null, "org.apache.unomi.persistence." + searchEngine, "throwExceptions", + updateConfiguration(null, persistenceConfigPid(), "throwExceptions", throwExceptionCurrent); } } diff --git a/itests/src/test/java/org/apache/unomi/itests/ProfileServiceWithoutOverwriteIT.java b/itests/src/test/java/org/apache/unomi/itests/ProfileServiceWithoutOverwriteIT.java index 2f94dd198..922b08bf9 100644 --- a/itests/src/test/java/org/apache/unomi/itests/ProfileServiceWithoutOverwriteIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/ProfileServiceWithoutOverwriteIT.java @@ -44,8 +44,7 @@ public class ProfileServiceWithoutOverwriteIT extends BaseIT { @Configuration public Option[] config() { - - searchEngine = System.getProperty(SEARCH_ENGINE_PROPERTY, SEARCH_ENGINE_ELASTICSEARCH); + resolvePersistenceBackend(); System.out.println("Search Engine: " + searchEngine); List<Option> options = new ArrayList<>(); diff --git a/itests/src/test/java/org/apache/unomi/itests/RolloverIT.java b/itests/src/test/java/org/apache/unomi/itests/RolloverIT.java index 449bfa278..667070dde 100644 --- a/itests/src/test/java/org/apache/unomi/itests/RolloverIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/RolloverIT.java @@ -18,8 +18,12 @@ package org.apache.unomi.itests; import com.fasterxml.jackson.databind.JsonNode; import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.unomi.itests.persistence.PersistenceITCapabilities; +import org.apache.unomi.itests.persistence.SearchBackendIT; import org.apache.unomi.shell.migration.utils.HttpUtils; +import org.junit.Assume; import org.junit.Test; +import org.junit.experimental.categories.Category; import org.junit.runner.RunWith; import org.ops4j.pax.exam.junit.PaxExam; import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; @@ -31,6 +35,7 @@ import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; /** * Verifies that Unomi correctly wires up the event index's rollover lifecycle on both Elasticsearch (ILM) @@ -52,9 +57,14 @@ import static org.junit.Assert.assertTrue; * that happens ISM has nothing left to transition to (the rollover policy's single state has no further * transitions) and marks that now-rolled-over index's management as completed/disabled, which would make a * hardcoded context-event-000001 check fail even though the rollover machinery worked correctly. + * <p> + * Non-search providers (e.g. PostgreSQL) leave {@link PersistenceITCapabilities.IndexRolloverApi#NONE} + * so this test is {@link Assume Assumed} skipped (not failed). Kept in {@link CorePersistenceITs} + * / {@link AllITs} for maximum suite coverage across backends. */ @RunWith(PaxExam.class) @ExamReactorStrategy(PerSuite.class) +@Category(SearchBackendIT.class) public class RolloverIT extends BaseIT { private static final String EVENT_ALIAS = "context-event"; @@ -63,16 +73,36 @@ public class RolloverIT extends BaseIT { @Test public void testEventIndexRolloverIsProperlyConfigured() throws Exception { + PersistenceITCapabilities caps = persistenceCapabilities(); + Assume.assumeTrue( + "Index rollover requires an HTTP rollover API (provider=" + + getPersistenceBackend().providerId() + ")", + caps.indexRolloverApi().isPresent()); + Assume.assumeTrue( + "Index rollover assertions require httpAdminApi (provider=" + + getPersistenceBackend().providerId() + ")", + caps.httpAdminApi()); + try (CloseableHttpClient client = createSearchEngineHttpClient()) { String writeIndex = resolveCurrentWriteIndex(client); JsonNode indexRoot = getJson(client, "/" + writeIndex + "/_settings?flat_settings=true").get(writeIndex); assertTrue("Expected the event write index " + writeIndex + " to already exist", indexRoot != null); JsonNode settings = indexRoot.get("settings"); - if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) { - assertOpenSearchRolloverConfigured(client, settings, writeIndex); - } else { - assertElasticsearchRolloverConfigured(client, settings, writeIndex); + switch (caps.indexRolloverApi()) { + case STATE_MANAGEMENT: + assertStateManagementRolloverConfigured(client, settings, writeIndex); + break; + case LIFECYCLE: + assertLifecycleRolloverConfigured(client, settings, writeIndex); + break; + case NONE: + fail("unreachable: indexRolloverApi was assumed present"); + break; + default: { + PersistenceITCapabilities.IndexRolloverApi unexpected = caps.indexRolloverApi(); + throw new IllegalStateException("Unhandled IndexRolloverApi: " + unexpected); + } } } } @@ -94,7 +124,7 @@ public class RolloverIT extends BaseIT { throw new AssertionError("Could not find a write index for alias " + EVENT_ALIAS); } - private void assertOpenSearchRolloverConfigured(CloseableHttpClient client, JsonNode settings, String writeIndex) throws IOException { + private void assertStateManagementRolloverConfigured(CloseableHttpClient client, JsonNode settings, String writeIndex) throws IOException { assertEquals("event index should reference the Unomi rollover policy", POLICY_ID, text(settings, "index.plugins.index_state_management.policy_id")); assertEquals("event index rollover_alias should be the event write alias", @@ -116,7 +146,7 @@ public class RolloverIT extends BaseIT { assertEquals(EXPECTED_MAX_DOCS, rolloverAction.get("min_doc_count").asLong()); } - private void assertElasticsearchRolloverConfigured(CloseableHttpClient client, JsonNode settings, String writeIndex) throws IOException { + private void assertLifecycleRolloverConfigured(CloseableHttpClient client, JsonNode settings, String writeIndex) throws IOException { assertEquals("event index should reference the Unomi rollover policy", POLICY_ID, text(settings, "index.lifecycle.name")); assertEquals("event index rollover_alias should be the event write alias", diff --git a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java index 214f71fa8..bd03f8436 100644 --- a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java @@ -26,19 +26,23 @@ import org.apache.unomi.api.conditions.ConditionType; import org.apache.unomi.api.tenants.Tenant; import org.apache.unomi.geonames.services.GeonameEntry; import org.apache.unomi.itests.BaseIT; +import org.apache.unomi.itests.persistence.SearchBackendIT; import org.apache.unomi.persistence.spi.aggregate.TermsAggregate; import org.apache.unomi.shell.migration.utils.HttpUtils; import org.apache.unomi.shell.migration.utils.MigrationUtils; import org.junit.After; import org.junit.Assert; +import org.junit.Assume; import org.junit.Before; import org.junit.Test; +import org.junit.experimental.categories.Category; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.*; +@Category(SearchBackendIT.class) public class Migrate16xToCurrentVersionIT extends BaseIT { private final static Logger LOGGER = LoggerFactory.getLogger(Migrate16xToCurrentVersionIT.class); @@ -54,7 +58,7 @@ public class Migrate16xToCurrentVersionIT extends BaseIT { // Elasticsearch connection constants private static String getEsBaseUrl() { - return "http://localhost:" + getSearchPort(); + return getSearchEngineBaseUrl(); } private static String getEsSnapshotRepo() { return getEsBaseUrl() + "/_snapshot/snapshots_repository/"; @@ -109,8 +113,9 @@ public class Migrate16xToCurrentVersionIT extends BaseIT { // This is called from BaseIT and will run before any migration setup checkSearchEngine(); - if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) { - System.out.println("Migration from 1.x to 2.x not supported for OpenSearch, skipping snapshot restore"); + if (!persistenceCapabilities().snapshotRestoreMigration()) { + System.out.println("snapshotRestoreMigration not supported for provider " + + getPersistenceBackend().providerId() + " — starting Unomi without legacy snapshot restore"); super.waitForStartup(); return; } @@ -119,7 +124,7 @@ public class Migrate16xToCurrentVersionIT extends BaseIT { LOGGER.info("Restoring snapshot into search engine..."); // Restore snapshot from 1.6.x - try (CloseableHttpClient httpClient = HttpUtils.initHttpClient(true, null)) { + try (CloseableHttpClient httpClient = createSearchEngineHttpClient()) { // Create snapshot repo HttpUtils.executePutRequest(httpClient, getEsSnapshotRepo(), resourceAsString(RESOURCE_CREATE_SNAPSHOTS_REPO), null); // Get snapshot, insure it exists @@ -199,10 +204,10 @@ public class Migrate16xToCurrentVersionIT extends BaseIT { */ @Test public void checkMigratedData() throws Exception { - if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) { - System.out.println("Migration from 1.x to 2.x not supported for OpenSearch, skipping checks"); - return; - } + Assume.assumeTrue( + "snapshotRestoreMigration not supported for provider " + + getPersistenceBackend().providerId(), + persistenceCapabilities().snapshotRestoreMigration()); checkMergedProfilesAliases(); checkProfileInterests(); checkProfileTotalNbOfVisits(); @@ -654,10 +659,10 @@ public class Migrate16xToCurrentVersionIT extends BaseIT { * Test that the default tenant was created during migration (migrate-3.1.0-10-tenantInitialization) */ private void checkDefaultTenantCreated() throws Exception { - if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) { - System.out.println("Migration from 1.x to 2.x not supported for OpenSearch, skipping checks"); - return; - } + Assume.assumeTrue( + "snapshotRestoreMigration not supported for provider " + + getPersistenceBackend().providerId(), + persistenceCapabilities().snapshotRestoreMigration()); // Check that the default tenant index exists Assert.assertTrue("Default tenant index should exist", MigrationUtils.indexExists(httpClient, getEsBaseUrl(), INDEX_PREFIX_CONTEXT + "tenant")); @@ -692,10 +697,10 @@ public class Migrate16xToCurrentVersionIT extends BaseIT { * have proper tenant information, audit metadata, and are accessible via definitionsService. */ private void checkDefinitionsServiceObjectsAccessible() throws Exception { - if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) { - System.out.println("Migration from 1.x to 2.x not supported for OpenSearch, skipping checks"); - return; - } + Assume.assumeTrue( + "snapshotRestoreMigration not supported for provider " + + getPersistenceBackend().providerId(), + persistenceCapabilities().snapshotRestoreMigration()); // Refresh the definitions service cache to ensure migrated items are loaded // This is necessary because items might be in persistence but not yet in cache @@ -811,10 +816,10 @@ public class Migrate16xToCurrentVersionIT extends BaseIT { * all condition types that use legacy *ESQueryBuilder syntax to use the new generic QueryBuilder syntax. */ private void checkLegacyQueryBuilderMigration() throws Exception { - if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) { - System.out.println("Migration from 1.x to 2.x not supported for OpenSearch, skipping checks"); - return; - } + Assume.assumeTrue( + "snapshotRestoreMigration not supported for provider " + + getPersistenceBackend().providerId(), + persistenceCapabilities().snapshotRestoreMigration()); // Refresh the definitions service cache to ensure migrated items are loaded definitionsService.refresh(); diff --git a/itests/src/test/java/org/apache/unomi/itests/persistence/ElasticsearchITBackend.java b/itests/src/test/java/org/apache/unomi/itests/persistence/ElasticsearchITBackend.java new file mode 100644 index 000000000..467c9d0fd --- /dev/null +++ b/itests/src/test/java/org/apache/unomi/itests/persistence/ElasticsearchITBackend.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package org.apache.unomi.itests.persistence; + +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.unomi.shell.migration.utils.HttpUtils; +import org.ops4j.pax.exam.Option; + +import java.io.IOException; + +import static org.ops4j.pax.exam.CoreOptions.maven; +import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.editConfigurationFilePut; +import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.features; + +/** + * Built-in Elasticsearch backend for Unomi integration tests. + */ +public class ElasticsearchITBackend implements PersistenceITBackend { + + @Override + public String providerId() { + return PersistenceITBackendResolver.PROVIDER_ELASTICSEARCH; + } + + @Override + public Option[] featureOptions() { + return new Option[]{ + features( + maven().groupId("org.apache.unomi").artifactId("unomi-kar").versionAsInProject().type("xml").classifier("features"), + "unomi-base", + "unomi-startup", + "unomi-elasticsearch-core", + "unomi-persistence-core", + "unomi-services", + "unomi-cxs-privacy-extension-services", + "unomi-plugins-base", + "unomi-plugins-request", + "unomi-plugins-mail", + "unomi-plugins-optimization-test", + "unomi-rest-api", + "unomi-cxs-privacy-extension", + "unomi-elasticsearch-conditions", + "unomi-cxs-lists-extension", + "unomi-cxs-geonames-extension", + "unomi-shell-dev-commands", + "unomi-wab", + "unomi-web-tracker", + "unomi-healthcheck-elasticsearch", + "unomi-router-karaf-feature", + "unomi-groovy-actions", + "unomi-rest-ui", + "cdp-graphql-feature", + "unomi-startup-complete" + ), + features( + maven().groupId("org.apache.unomi").artifactId("unomi-distribution").versionAsInProject().type("xml").classifier("features"), + "unomi-distribution-elasticsearch-graphql" + ) + }; + } + + @Override + public Option[] configurationOptions() { + String port = searchPort(); + return new Option[]{ + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.cluster.name", "contextElasticSearchITests"), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.addresses", "localhost:" + port), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.taskWaitingPollingInterval", "50"), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.rollover.maxDocs", "300"), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.minimalClusterState", "YELLOW"), + }; + } + + @Override + public String distributionFeature() { + return "unomi-distribution-elasticsearch"; + } + + @Override + public String persistenceConfigPid() { + return "org.apache.unomi.persistence.elasticsearch"; + } + + @Override + public PersistenceITCapabilities capabilities() { + return PersistenceITCapabilities.elasticsearch(); + } + + @Override + public String searchBaseUrl() { + return "http://localhost:" + searchPort(); + } + + @Override + public String searchPort() { + return System.getProperty("elasticsearch.port", "9400"); + } + + @Override + public CloseableHttpClient createSearchHttpClient() throws IOException { + return HttpUtils.initHttpClient(true, null); + } +} diff --git a/itests/src/test/java/org/apache/unomi/itests/persistence/OpenSearchITBackend.java b/itests/src/test/java/org/apache/unomi/itests/persistence/OpenSearchITBackend.java new file mode 100644 index 000000000..04e182264 --- /dev/null +++ b/itests/src/test/java/org/apache/unomi/itests/persistence/OpenSearchITBackend.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package org.apache.unomi.itests.persistence; + +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.unomi.shell.migration.utils.HttpUtils; +import org.ops4j.pax.exam.Option; + +import java.io.IOException; + +import static org.ops4j.pax.exam.CoreOptions.maven; +import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.editConfigurationFilePut; +import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.features; + +/** + * Built-in OpenSearch backend for Unomi integration tests. + */ +public class OpenSearchITBackend implements PersistenceITBackend { + + private static final String OS_USER = "admin"; + private static final String OS_PASSWORD = "Unomi.1ntegrat10n.Tests"; + + @Override + public String providerId() { + return PersistenceITBackendResolver.PROVIDER_OPENSEARCH; + } + + @Override + public Option[] featureOptions() { + return new Option[]{ + features( + maven().groupId("org.apache.unomi").artifactId("unomi-kar").versionAsInProject().type("xml").classifier("features"), + "unomi-base", + "unomi-startup", + "unomi-opensearch-core", + "unomi-persistence-core", + "unomi-services", + "unomi-cxs-privacy-extension-services", + "unomi-plugins-base", + "unomi-plugins-request", + "unomi-plugins-mail", + "unomi-plugins-optimization-test", + "unomi-rest-api", + "unomi-cxs-privacy-extension", + "unomi-opensearch-conditions", + "unomi-cxs-lists-extension", + "unomi-cxs-geonames-extension", + "unomi-shell-dev-commands", + "unomi-wab", + "unomi-web-tracker", + "unomi-healthcheck-opensearch", + "unomi-router-karaf-feature", + "unomi-groovy-actions", + "unomi-rest-ui", + "cdp-graphql-feature", + "unomi-startup-complete" + ), + features( + maven().groupId("org.apache.unomi").artifactId("unomi-distribution").versionAsInProject().type("xml").classifier("features"), + "unomi-distribution-opensearch-graphql" + ) + }; + } + + @Override + public Option[] configurationOptions() { + String port = searchPort(); + return new Option[]{ + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.cluster.name", "contextElasticSearchITests"), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.addresses", "localhost:" + port), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.username", OS_USER), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.password", OS_PASSWORD), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.sslEnable", "false"), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.sslTrustAllCertificates", "true"), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.rollover.maxDocs", "300"), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.minimalClusterState", "YELLOW"), + }; + } + + @Override + public String distributionFeature() { + return "unomi-distribution-opensearch"; + } + + @Override + public String persistenceConfigPid() { + return "org.apache.unomi.persistence.opensearch"; + } + + @Override + public PersistenceITCapabilities capabilities() { + return PersistenceITCapabilities.opensearch(); + } + + @Override + public String searchBaseUrl() { + return "http://localhost:" + searchPort(); + } + + @Override + public String searchPort() { + return System.getProperty("org.apache.unomi.opensearch.addresses", "localhost:9401") + .split(":")[1]; + } + + @Override + public CloseableHttpClient createSearchHttpClient() throws IOException { + BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, + new UsernamePasswordCredentials(OS_USER, OS_PASSWORD)); + return HttpUtils.initHttpClient(true, credentialsProvider); + } +} diff --git a/itests/src/test/java/org/apache/unomi/itests/persistence/PersistenceITBackend.java b/itests/src/test/java/org/apache/unomi/itests/persistence/PersistenceITBackend.java new file mode 100644 index 000000000..50beaa657 --- /dev/null +++ b/itests/src/test/java/org/apache/unomi/itests/persistence/PersistenceITBackend.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package org.apache.unomi.itests.persistence; + +import org.apache.http.impl.client.CloseableHttpClient; +import org.osgi.framework.BundleContext; +import org.osgi.service.cm.ConfigurationAdmin; +import org.ops4j.pax.exam.Option; + +import java.io.IOException; + +/** + * Test-only contract for provisioning and awaiting a {@code PersistenceService} provider + * during Pax Exam integration tests. + * <p> + * Built-in implementations cover Elasticsearch and OpenSearch. Additional providers register + * via {@link java.util.ServiceLoader} or {@code -Dunomi.persistence.it.backend=<fqcn>}. + */ +public interface PersistenceITBackend { + + /** Provider id (e.g. {@code elasticsearch}, {@code opensearch}). */ + String providerId(); + + /** + * Karaf feature options for this provider (kar features + distribution feature). + */ + Option[] featureOptions(); + + /** + * Provider-specific {@code etc/custom.system.properties} (and related) Pax Exam options. + */ + Option[] configurationOptions(); + + /** Distribution feature name passed to {@code unomi:setup -d=…}. */ + String distributionFeature(); + + /** + * Wait until the backend is usable before Unomi starts (cluster health, JDBC ping, …). + * Default is a no-op; search backends typically poll HTTP health from {@code BaseIT}. + * <p> + * Called from the Pax Exam driver / early {@code @Before} path — do not assume OSGi + * services are available yet. Prefer {@link #prepareBeforeUnomiSetup} for ConfigAdmin work. + */ + default void awaitBackendReady() throws Exception { + } + + /** + * Called in the Karaf JVM after {@code UnomiManagementService} is available and before + * {@code unomi:setup}. Use for ConfigAdmin DataSource patching, bundle restarts, etc. + */ + default void prepareBeforeUnomiSetup(BundleContext bundleContext, ConfigurationAdmin configurationAdmin) + throws Exception { + } + + /** Optional verification after Unomi has started. */ + default void assertHealthyAfterUnomiStart() throws Exception { + } + + /** ConfigAdmin PID for {@code org.apache.unomi.persistence.*} knobs (e.g. {@code throwExceptions}). */ + String persistenceConfigPid(); + + PersistenceITCapabilities capabilities(); + + /** HTTP base URL when {@link PersistenceITCapabilities#httpAdminApi()} is true. */ + default String searchBaseUrl() { + throw new UnsupportedOperationException(providerId() + " does not expose an HTTP admin API"); + } + + /** Search HTTP listen port when {@link PersistenceITCapabilities#httpAdminApi()} is true. */ + default String searchPort() { + throw new UnsupportedOperationException(providerId() + " does not expose an HTTP admin API"); + } + + /** HTTP client for admin APIs (may include basic auth). */ + default CloseableHttpClient createSearchHttpClient() throws IOException { + throw new UnsupportedOperationException(providerId() + " does not expose an HTTP admin API"); + } +} diff --git a/itests/src/test/java/org/apache/unomi/itests/persistence/PersistenceITBackendResolver.java b/itests/src/test/java/org/apache/unomi/itests/persistence/PersistenceITBackendResolver.java new file mode 100644 index 000000000..5c8466237 --- /dev/null +++ b/itests/src/test/java/org/apache/unomi/itests/persistence/PersistenceITBackendResolver.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package org.apache.unomi.itests.persistence; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; + +/** + * Resolves the active {@link PersistenceITBackend} for the IT harness. + * <p> + * Resolution order: + * <ol> + * <li>{@code -Dunomi.persistence.it.backend=<fully.qualified.ClassName>}</li> + * <li>{@link ServiceLoader} implementations matching the provider id</li> + * <li>Built-in Elasticsearch / OpenSearch backends</li> + * </ol> + * Provider id comes from {@code unomi.persistence.provider}, falling back to the + * deprecated {@code unomi.search.engine} alias (default {@code elasticsearch}). + */ +public final class PersistenceITBackendResolver { + + public static final String PROVIDER_PROPERTY = "unomi.persistence.provider"; + /** @deprecated use {@link #PROVIDER_PROPERTY}; kept for existing scripts and CI. */ + @Deprecated + public static final String SEARCH_ENGINE_PROPERTY = "unomi.search.engine"; + public static final String BACKEND_CLASS_PROPERTY = "unomi.persistence.it.backend"; + + public static final String PROVIDER_ELASTICSEARCH = "elasticsearch"; + public static final String PROVIDER_OPENSEARCH = "opensearch"; + + private PersistenceITBackendResolver() { + } + + public static String resolveProviderId() { + String provider = System.getProperty(PROVIDER_PROPERTY); + if (provider != null && !provider.isBlank()) { + return provider.trim(); + } + return System.getProperty(SEARCH_ENGINE_PROPERTY, PROVIDER_ELASTICSEARCH).trim(); + } + + public static PersistenceITBackend resolve() { + String explicitClass = System.getProperty(BACKEND_CLASS_PROPERTY); + if (explicitClass != null && !explicitClass.isBlank()) { + return instantiate(explicitClass.trim()); + } + + String providerId = resolveProviderId(); + Map<String, PersistenceITBackend> byId = loadBackendsById(); + PersistenceITBackend backend = byId.get(providerId); + if (backend != null) { + return backend; + } + + throw new IllegalArgumentException( + "No PersistenceITBackend registered for provider '" + providerId + "'. " + + "Set -D" + PROVIDER_PROPERTY + "=<id> (or deprecated -D" + SEARCH_ENGINE_PROPERTY + "), " + + "register a ServiceLoader implementation, or set -D" + BACKEND_CLASS_PROPERTY + "=<fqcn>. " + + "Known built-in / discovered ids: " + byId.keySet()); + } + + private static Map<String, PersistenceITBackend> loadBackendsById() { + Map<String, PersistenceITBackend> byId = new LinkedHashMap<>(); + // Built-ins first so ServiceLoader can override the same id if desired. + register(byId, new ElasticsearchITBackend()); + register(byId, new OpenSearchITBackend()); + for (PersistenceITBackend backend : ServiceLoader.load(PersistenceITBackend.class)) { + register(byId, backend); + } + return byId; + } + + private static void register(Map<String, PersistenceITBackend> byId, PersistenceITBackend backend) { + byId.put(backend.providerId(), backend); + } + + private static PersistenceITBackend instantiate(String fqcn) { + try { + Class<?> clazz = Class.forName(fqcn); + if (!PersistenceITBackend.class.isAssignableFrom(clazz)) { + throw new IllegalArgumentException(fqcn + " does not implement PersistenceITBackend"); + } + return (PersistenceITBackend) clazz.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new IllegalArgumentException("Failed to instantiate PersistenceITBackend " + fqcn, e); + } + } + + /** Visible for tests / diagnostics. */ + static List<String> knownProviderIds() { + return new ArrayList<>(loadBackendsById().keySet()); + } +} diff --git a/itests/src/test/java/org/apache/unomi/itests/persistence/PersistenceITCapabilities.java b/itests/src/test/java/org/apache/unomi/itests/persistence/PersistenceITCapabilities.java new file mode 100644 index 000000000..707bc4301 --- /dev/null +++ b/itests/src/test/java/org/apache/unomi/itests/persistence/PersistenceITCapabilities.java @@ -0,0 +1,204 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package org.apache.unomi.itests.persistence; + +/** + * Backend-agnostic IT capabilities. Describe <em>what</em> the harness and tests may + * rely on (HTTP admin surface, rollover API shape, health probes), not which vendor + * product is installed. + * <p> + * Built-in Elasticsearch / OpenSearch factories set these to match today’s search + * engines; JDBC and other providers typically use {@link #none()}. + */ +public final class PersistenceITCapabilities { + + /** + * HTTP API shape used to inspect or assert index rollover policy wiring. + * Named after the protocol, not a vendor. + */ + public enum IndexRolloverApi { + /** No rollover HTTP API (typical for SQL / embedded stores). */ + NONE, + /** + * Lifecycle-policy HTTP API (paths/settings of the form {@code _ilm}, + * {@code index.lifecycle.*}). + */ + LIFECYCLE, + /** + * State-management-policy HTTP API (paths/settings of the form + * {@code _plugins/_ism}, {@code index_state_management.*}). + */ + STATE_MANAGEMENT; + + public boolean isPresent() { + return this != NONE; + } + } + + /** + * Expected outcome of range queries on flattened / nested document properties. + */ + public enum FlattenedRangeQueryResult { + /** Query succeeds with null or an empty hit list. */ + EMPTY, + /** Query may return matching documents. */ + HITS + } + + private final boolean httpAdminApi; + private final boolean providerNamedHealthProbe; + private final boolean clusterHealthProbe; + private final IndexRolloverApi indexRolloverApi; + private final boolean snapshotRestoreMigration; + private final FlattenedRangeQueryResult flattenedRangeQueryResult; + + private PersistenceITCapabilities(Builder builder) { + this.httpAdminApi = builder.httpAdminApi; + this.providerNamedHealthProbe = builder.providerNamedHealthProbe; + this.clusterHealthProbe = builder.clusterHealthProbe; + this.indexRolloverApi = builder.indexRolloverApi; + this.snapshotRestoreMigration = builder.snapshotRestoreMigration; + this.flattenedRangeQueryResult = builder.flattenedRangeQueryResult; + } + + /** + * Backend exposes HTTP admin APIs for cluster/index/snapshot operations + * (typical of search engines). When false, {@code BaseIT} skips HTTP health + * prep and search helpers refuse to run. + */ + public boolean httpAdminApi() { + return httpAdminApi; + } + + /** + * Health check includes a probe whose name equals the persistence provider id. + */ + public boolean providerNamedHealthProbe() { + return providerNamedHealthProbe; + } + + /** + * Health check includes a cluster-level probe (name {@code cluster}). + */ + public boolean clusterHealthProbe() { + return clusterHealthProbe; + } + + /** + * Rollover policy HTTP API shape, or {@link IndexRolloverApi#NONE}. + */ + public IndexRolloverApi indexRolloverApi() { + return indexRolloverApi; + } + + /** + * Backend can exercise legacy snapshot-restore migration fixtures + * (search-engine snapshot HTTP + restore into current indices). + */ + public boolean snapshotRestoreMigration() { + return snapshotRestoreMigration; + } + + /** + * How range queries on flattened properties behave for assertions. + */ + public FlattenedRangeQueryResult flattenedRangeQueryResult() { + return flattenedRangeQueryResult; + } + + public static Builder builder() { + return new Builder(); + } + + /** No optional capabilities — JDBC, embedded, or incomplete providers. */ + public static PersistenceITCapabilities none() { + return builder().build(); + } + + /** @deprecated use {@link #none()} */ + @Deprecated + public static PersistenceITCapabilities nonSearchBackend() { + return none(); + } + + public static PersistenceITCapabilities elasticsearch() { + return builder() + .httpAdminApi(true) + .providerNamedHealthProbe(true) + .clusterHealthProbe(true) + .indexRolloverApi(IndexRolloverApi.LIFECYCLE) + .snapshotRestoreMigration(true) + .flattenedRangeQueryResult(FlattenedRangeQueryResult.EMPTY) + .build(); + } + + public static PersistenceITCapabilities opensearch() { + return builder() + .httpAdminApi(true) + .providerNamedHealthProbe(true) + .clusterHealthProbe(true) + .indexRolloverApi(IndexRolloverApi.STATE_MANAGEMENT) + .snapshotRestoreMigration(false) + .flattenedRangeQueryResult(FlattenedRangeQueryResult.HITS) + .build(); + } + + public static final class Builder { + private boolean httpAdminApi; + private boolean providerNamedHealthProbe; + private boolean clusterHealthProbe; + private IndexRolloverApi indexRolloverApi = IndexRolloverApi.NONE; + private boolean snapshotRestoreMigration; + private FlattenedRangeQueryResult flattenedRangeQueryResult = FlattenedRangeQueryResult.EMPTY; + + public Builder httpAdminApi(boolean httpAdminApi) { + this.httpAdminApi = httpAdminApi; + return this; + } + + public Builder providerNamedHealthProbe(boolean providerNamedHealthProbe) { + this.providerNamedHealthProbe = providerNamedHealthProbe; + return this; + } + + public Builder clusterHealthProbe(boolean clusterHealthProbe) { + this.clusterHealthProbe = clusterHealthProbe; + return this; + } + + public Builder indexRolloverApi(IndexRolloverApi indexRolloverApi) { + this.indexRolloverApi = indexRolloverApi != null ? indexRolloverApi : IndexRolloverApi.NONE; + return this; + } + + public Builder snapshotRestoreMigration(boolean snapshotRestoreMigration) { + this.snapshotRestoreMigration = snapshotRestoreMigration; + return this; + } + + public Builder flattenedRangeQueryResult(FlattenedRangeQueryResult flattenedRangeQueryResult) { + this.flattenedRangeQueryResult = flattenedRangeQueryResult != null + ? flattenedRangeQueryResult + : FlattenedRangeQueryResult.EMPTY; + return this; + } + + public PersistenceITCapabilities build() { + return new PersistenceITCapabilities(this); + } + } +} diff --git a/itests/src/test/java/org/apache/unomi/itests/persistence/SearchBackendIT.java b/itests/src/test/java/org/apache/unomi/itests/persistence/SearchBackendIT.java new file mode 100644 index 000000000..9f8f81820 --- /dev/null +++ b/itests/src/test/java/org/apache/unomi/itests/persistence/SearchBackendIT.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package org.apache.unomi.itests.persistence; + +/** + * Marker category for ITs whose <em>primary</em> assertions need HTTP admin APIs + * (snapshot restore, index rollover policy HTTP). Prefer keeping these classes in + * {@link org.apache.unomi.itests.CorePersistenceITs} / {@link org.apache.unomi.itests.AllITs} + * and gating with {@link org.junit.Assume} on {@link PersistenceITCapabilities} so unsupported + * backends skip rather than losing suite membership. + * <p> + * Optional Failsafe {@code excludedGroups} may still list this category for specialized cells. + */ +public interface SearchBackendIT { +} diff --git a/itests/src/test/resources/META-INF/services/org.apache.unomi.itests.persistence.PersistenceITBackend b/itests/src/test/resources/META-INF/services/org.apache.unomi.itests.persistence.PersistenceITBackend new file mode 100644 index 000000000..46eb7e9d3 --- /dev/null +++ b/itests/src/test/resources/META-INF/services/org.apache.unomi.itests.persistence.PersistenceITBackend @@ -0,0 +1,2 @@ +org.apache.unomi.itests.persistence.ElasticsearchITBackend +org.apache.unomi.itests.persistence.OpenSearchITBackend
