From d15802ebcfcec63854781f99a311c33fb28f4f39 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Wed, 7 Oct 2026 13:27:49 +0200 Subject: [PATCH 1/6] UNOMI-998: Cover a 3.0 to 4.0 migration in the integration tests Add a synthetic 3.0 seed that runs unomi:migrate before Unomi starts, and correct the tenant API key file header names written by the migration. --- .../java/org/apache/unomi/itests/AllITs.java | 2 + .../unomi/itests/CorePersistenceITs.java | 2 + .../Migrate30ToCurrentVersionIT.java | 347 ++++++++++++++++++ ...grate-4.0.0-10-tenantInitialization.groovy | 4 +- 4 files changed, 353 insertions(+), 2 deletions(-) create mode 100644 itests/src/test/java/org/apache/unomi/itests/migration/Migrate30ToCurrentVersionIT.java 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 b072a31fd..76c1ad054 100644 --- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java +++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java @@ -18,6 +18,7 @@ import org.apache.unomi.itests.migration.Migrate16xToCurrentVersionIT; import org.apache.unomi.itests.graphql.*; +import org.apache.unomi.itests.migration.Migrate30ToCurrentVersionIT; import org.apache.unomi.itests.migration.Migrate40IndexMappingIT; import org.apache.unomi.itests.migration.Migrate40TenantAssignmentIT; import org.apache.unomi.itests.migration.MigrationIT; @@ -34,6 +35,7 @@ */ @RunWith(ProgressSuite.class) @SuiteClasses({ + Migrate30ToCurrentVersionIT.class, Migrate16xToCurrentVersionIT.class, MigrationIT.class, Migrate40IndexMappingIT.class, diff --git a/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java b/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java index 6f3c510ef..38d88b17a 100644 --- a/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java +++ b/itests/src/test/java/org/apache/unomi/itests/CorePersistenceITs.java @@ -18,6 +18,7 @@ import org.apache.unomi.itests.migration.Migrate16xToCurrentVersionIT; import org.apache.unomi.itests.graphql.*; +import org.apache.unomi.itests.migration.Migrate30ToCurrentVersionIT; import org.apache.unomi.itests.migration.Migrate40IndexMappingIT; import org.apache.unomi.itests.migration.Migrate40TenantAssignmentIT; import org.apache.unomi.itests.migration.MigrationIT; @@ -35,6 +36,7 @@ */ @RunWith(ProgressSuite.class) @SuiteClasses({ + Migrate30ToCurrentVersionIT.class, Migrate16xToCurrentVersionIT.class, MigrationIT.class, Migrate40IndexMappingIT.class, diff --git a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate30ToCurrentVersionIT.java b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate30ToCurrentVersionIT.java new file mode 100644 index 000000000..11fe95356 --- /dev/null +++ b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate30ToCurrentVersionIT.java @@ -0,0 +1,347 @@ +/* + * 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.migration; + +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.unomi.api.conditions.ConditionType; +import org.apache.unomi.api.tenants.Tenant; +import org.apache.unomi.itests.BaseIT; +import org.apache.unomi.itests.persistence.SearchBackendIT; +import org.apache.unomi.shell.migration.utils.HttpUtils; +import org.apache.unomi.shell.migration.utils.MigrationUtils; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; + +/** + * UNOMI-998: migrate a synthetic Unomi 3.0 data set with {@code unomi:migrate 3.0.0} + * before Unomi starts, then assert the post-migration shape that 996/997 fixed. + *

+ * Must run before {@link Migrate16xToCurrentVersionIT} so that the 1.6 snapshot restore + * can wipe this fixture afterward. Uses HTTP seeding so both Elasticsearch and OpenSearch + * can run it ({@code httpAdminApi}), unlike the 1.6 snapshot path. + *

+ * One {@code @Test} method only: {@code @Before} must not re-seed on every method, or the + * second migrate is skipped by migration history and leaves documents without tenants. + *

+ * Pax Exam still installs the full feature set for the shared suite. The boot-feature + * mapping path is guarded by asserting that the migration command bundle itself ships + * the engine mappings (UNOMI-996). + */ +@Category(SearchBackendIT.class) +public class Migrate30ToCurrentVersionIT extends BaseIT { + + private static final Logger LOGGER = LoggerFactory.getLogger(Migrate30ToCurrentVersionIT.class); + + private static final String MIGRATION_COMMAND = "unomi:migrate 3.0.0 true"; + private static final long MIGRATION_TIMEOUT = 900000L; + + private static final String INDEX_PREFIX = "context-"; + private static final String INDEX_SYSTEMITEMS = INDEX_PREFIX + "systemitems"; + private static final String INDEX_PROFILE = INDEX_PREFIX + "profile"; + private static final String INDEX_GEONAME = INDEX_PREFIX + "geonameentry"; + private static final String INDEX_CLUSTERNODE = INDEX_PREFIX + "clusternode"; + private static final String INDEX_SFDC = INDEX_PREFIX + "sfdcconfiguration"; + private static final String INDEX_TENANT = INDEX_PREFIX + "tenant"; + + private static final String USER_RULE_ID = "unomi998-user-rule"; + private static final String USER_SEGMENT_ID = "unomi998-user-segment"; + private static final String GEO_ID = "unomi998-paris"; + private static final String NODE_ID = "unomi998-node"; + private static final String SFDC_ID = "unomi998-sfdc"; + private static final String PROFILE_ID = "unomi998-profile"; + private static final String OLD_CONDITION_MARKER = "unomi998-3.0-seed-booleanCondition"; + + private static final String SIMPLE_MAPPING = "{" + + "\"settings\":{\"number_of_shards\":1,\"number_of_replicas\":0}," + + "\"mappings\":{\"properties\":{" + + "\"itemId\":{\"type\":\"keyword\"}," + + "\"itemType\":{\"type\":\"keyword\"}," + + "\"scope\":{\"type\":\"keyword\"}," + + "\"tenantId\":{\"type\":\"keyword\"}," + + "\"metadata\":{\"type\":\"object\",\"enabled\":true}," + + "\"name\":{\"type\":\"keyword\"}," + + "\"properties\":{\"type\":\"object\",\"enabled\":true}" + + "}}}"; + + private CloseableHttpClient httpClient; + + @Override + @Before + public void waitForStartup() throws InterruptedException { + checkSearchEngine(); + + if (!persistenceCapabilities().httpAdminApi()) { + System.out.println("httpAdminApi not supported for provider " + + getPersistenceBackend().providerId() + " — starting Unomi without 3.0 migrate seed"); + super.waitForStartup(); + return; + } + + try { + httpClient = createSearchEngineHttpClient(); + seedUnomi30Dataset(httpClient); + assertMigrationBundleShipsEngineMappings(); + + System.out.println("Launching migration from 3.0.0..."); + LOGGER.info("Launching migration from 3.0.0..."); + String migrationOutput = executeCommand(MIGRATION_COMMAND, MIGRATION_TIMEOUT, false); + System.out.println("Migration command output results:"); + System.out.println(migrationOutput); + Assert.assertNotNull(migrationOutput); + Assert.assertFalse("migration must not abort", + migrationOutput.toLowerCase().contains("migration process aborted")); + Assert.assertTrue("migration must run 4.0.0 tenant scripts", + migrationOutput.contains("tenantDocumentIds")); + Assert.assertTrue("migration must finish", + migrationOutput.contains("Finish execution of:") + || migrationOutput.contains("migrationStatus")); + + prepareSearchEngineAfterMigration(); + } catch (Throwable t) { + LOGGER.error("Error during 3.0 migration setup", t); + throw new RuntimeException("Error during 3.0 migration setup", t); + } + + super.waitForStartup(); + } + + @Test + public void checkMigratedDataFrom30() throws Exception { + Assume.assumeTrue( + "HTTP admin API required (provider=" + getPersistenceBackend().providerId() + ")", + persistenceCapabilities().httpAdminApi()); + + assertDefaultTenantExists(); + assertUserContentStaysOutOfSystemTenant(); + assertGeonamesClusterNodeAndSfdc(); + assertEverySeededDocumentHasATenant(); + assertShippedDefinitionsAreFourZero(); + assertApiKeyFileNamesCorrectHeaders(); + } + + private void assertDefaultTenantExists() throws Exception { + Assert.assertTrue("tenant index must exist", + MigrationUtils.indexExists(httpClient, getSearchEngineBaseUrl(), INDEX_TENANT)); + + Tenant tenant = tenantService.getTenant(TEST_TENANT_ID); + Assert.assertNotNull("default tenant must exist after migration (UNOMI-998)", tenant); + Assert.assertEquals(TEST_TENANT_ID, tenant.getItemId()); + Assert.assertNotNull("tenant must have a public API key", tenant.getPublicApiKey()); + Assert.assertNotNull("tenant must have a private API key", tenant.getPrivateApiKey()); + } + + private void assertUserContentStaysOutOfSystemTenant() throws Exception { + JsonNode rule = findByItemId(INDEX_SYSTEMITEMS, USER_RULE_ID); + Assert.assertNotNull("user rule must still exist", rule); + Assert.assertEquals(TEST_TENANT_ID, rule.path("_source").path("tenantId").asText()); + Assert.assertTrue(rule.path("_id").asText().startsWith(TEST_TENANT_ID + "_")); + + JsonNode segment = findByItemId(INDEX_SYSTEMITEMS, USER_SEGMENT_ID); + Assert.assertNotNull("user segment must still exist", segment); + Assert.assertEquals(TEST_TENANT_ID, segment.path("_source").path("tenantId").asText()); + Assert.assertFalse("user segment must not use the system document id", + segment.path("_id").asText().startsWith("system_")); + } + + private void assertGeonamesClusterNodeAndSfdc() throws Exception { + JsonNode geo = findByItemId(INDEX_GEONAME, GEO_ID); + Assert.assertNotNull(geo); + Assert.assertEquals("system", geo.path("_source").path("tenantId").asText()); + + JsonNode node = findByItemId(INDEX_CLUSTERNODE, NODE_ID); + Assert.assertNotNull(node); + Assert.assertEquals("cluster nodes stay in the configured tenant", TEST_TENANT_ID, + node.path("_source").path("tenantId").asText()); + + // Unknown index type uses forceDefaultTenant — literal scope "system" does not win + JsonNode sfdc = findByItemId(INDEX_SFDC, SFDC_ID); + Assert.assertNotNull(sfdc); + Assert.assertEquals(TEST_TENANT_ID, sfdc.path("_source").path("tenantId").asText()); + + JsonNode profile = findByItemId(INDEX_PROFILE, PROFILE_ID); + Assert.assertNotNull(profile); + Assert.assertEquals(TEST_TENANT_ID, profile.path("_source").path("tenantId").asText()); + } + + private void assertEverySeededDocumentHasATenant() throws Exception { + for (String index : Arrays.asList(INDEX_SYSTEMITEMS, INDEX_PROFILE, INDEX_GEONAME, INDEX_CLUSTERNODE, INDEX_SFDC, INDEX_TENANT)) { + if (!MigrationUtils.indexExists(httpClient, getSearchEngineBaseUrl(), index)) { + continue; + } + JsonNode hits = getObjectMapper().readTree(HttpUtils.executeGetRequest(httpClient, + getSearchEngineBaseUrl() + "/" + index + "/_search?size=100", null)) + .path("hits").path("hits"); + Assert.assertTrue(index + " should have documents", hits.size() > 0); + for (JsonNode hit : hits) { + Assert.assertTrue(index + " document " + hit.path("_id").asText() + " must have tenantId", + hit.path("_source").hasNonNull("tenantId") + && !hit.path("_source").path("tenantId").asText().isBlank()); + } + } + } + + private void assertShippedDefinitionsAreFourZero() throws Exception { + ConditionType booleanCondition = definitionsService.getConditionType("booleanCondition"); + Assert.assertNotNull("booleanCondition must be available after migration", booleanCondition); + String description = booleanCondition.getMetadata() != null + ? booleanCondition.getMetadata().getDescription() : null; + Assert.assertNotEquals("3.0 seeded marker must not win over the 4.0 bundle definition", + OLD_CONDITION_MARKER, description); + + // Post-2.2 document id for system condition types: system__conditiontype + JsonNode migratedShipped = getByDocumentId(INDEX_SYSTEMITEMS, "system_booleanCondition_conditiontype"); + if (migratedShipped == null) { + migratedShipped = findByItemId(INDEX_SYSTEMITEMS, "booleanCondition"); + } + Assert.assertNotNull("booleanCondition must exist in systemitems after migration", migratedShipped); + Assert.assertEquals("shipped definitions go to system", "system", + migratedShipped.path("_source").path("tenantId").asText()); + Assert.assertTrue("shipped definition document id must use the system tenant prefix", + migratedShipped.path("_id").asText().startsWith("system_")); + } + + private void assertApiKeyFileNamesCorrectHeaders() throws Exception { + Path secretsDir = Paths.get(karafData(), "migration", "secrets"); + Assert.assertTrue("migration secrets directory must exist: " + secretsDir, Files.isDirectory(secretsDir)); + + Path keyFile = null; + try (DirectoryStream stream = Files.newDirectoryStream(secretsDir, "tenant-api-keys-" + TEST_TENANT_ID + "-*.txt")) { + for (Path path : stream) { + keyFile = path; + break; + } + } + Assert.assertNotNull("migration must write a tenant API key file", keyFile); + + String content = Files.readString(keyFile, StandardCharsets.UTF_8); + Assert.assertTrue("public key must name X-Unomi-Api-Key\n" + content, + content.contains("X-Unomi-Api-Key")); + Assert.assertTrue("private key must mention Basic authentication\n" + content, + content.toLowerCase().contains("basic")); + Assert.assertFalse("must not advertise the non-existent X-Unomi-Public-Key header\n" + content, + content.contains("X-Unomi-Public-Key")); + Assert.assertFalse("must not advertise the non-existent X-Unomi-Key header\n" + content, + content.contains("X-Unomi-Key header")); + } + + private void seedUnomi30Dataset(CloseableHttpClient client) throws Exception { + String es = getSearchEngineBaseUrl(); + System.out.println("Seeding synthetic Unomi 3.0 dataset..."); + LOGGER.info("Seeding synthetic Unomi 3.0 dataset..."); + + for (String index : Arrays.asList(INDEX_SYSTEMITEMS, INDEX_PROFILE, INDEX_GEONAME, INDEX_CLUSTERNODE, INDEX_SFDC)) { + MigrationUtils.deleteIndex(client, es, index); + MigrationUtils.createIndex(client, es, index, SIMPLE_MAPPING); + } + + putDoc(client, INDEX_SYSTEMITEMS, "booleanCondition", + "{\"itemId\":\"booleanCondition\",\"itemType\":\"conditionType\"," + + "\"metadata\":{\"id\":\"booleanCondition\",\"description\":\"" + OLD_CONDITION_MARKER + "\"}}"); + putDoc(client, INDEX_SYSTEMITEMS, USER_RULE_ID, + "{\"itemId\":\"" + USER_RULE_ID + "\",\"itemType\":\"rule\",\"scope\":\"systemscope\"," + + "\"metadata\":{\"id\":\"" + USER_RULE_ID + "\",\"name\":\"UNOMI-998 user rule\"}}"); + putDoc(client, INDEX_SYSTEMITEMS, USER_SEGMENT_ID, + "{\"itemId\":\"" + USER_SEGMENT_ID + "\",\"itemType\":\"segment\",\"scope\":\"systemscope\"," + + "\"metadata\":{\"id\":\"" + USER_SEGMENT_ID + "\",\"name\":\"UNOMI-998 user segment\"}}"); + + putDoc(client, INDEX_GEONAME, GEO_ID, + "{\"itemId\":\"" + GEO_ID + "\",\"itemType\":\"geonameEntry\",\"name\":\"Paris\"}"); + putDoc(client, INDEX_CLUSTERNODE, NODE_ID, + "{\"itemId\":\"" + NODE_ID + "\",\"itemType\":\"clusterNode\",\"scope\":\"system\"}"); + putDoc(client, INDEX_SFDC, SFDC_ID, + "{\"itemId\":\"" + SFDC_ID + "\",\"itemType\":\"sfdcConfiguration\",\"scope\":\"system\"}"); + putDoc(client, INDEX_PROFILE, PROFILE_ID, + "{\"itemId\":\"" + PROFILE_ID + "\",\"itemType\":\"profile\",\"properties\":{\"firstName\":\"Ada\"}}"); + + HttpUtils.executePostRequest(client, es + "/_refresh", null, null); + } + + private void assertMigrationBundleShipsEngineMappings() { + Bundle migrationBundle = FrameworkUtil.getBundle(MigrationUtils.class); + Assert.assertNotNull(migrationBundle); + for (String engine : Arrays.asList("elasticsearch", "opensearch")) { + for (String mappingFile : Arrays.asList("clusterNode.json", "profile.json", "geonameEntry.json", "systemItems.json")) { + String path = "META-INF/cxs/migration-mappings/" + engine + "/" + mappingFile; + Assert.assertNotNull("boot migrate needs " + path + " in shell-commands", + migrationBundle.getEntry(path)); + } + } + } + + private void putDoc(CloseableHttpClient client, String index, String id, String source) throws Exception { + HttpUtils.executePutRequest(client, + getSearchEngineBaseUrl() + "/" + index + "/_doc/" + id + "?refresh=true", source, null); + } + + private JsonNode getByDocumentId(String index, String documentId) throws Exception { + try { + String response = HttpUtils.executeGetRequest(httpClient, + getSearchEngineBaseUrl() + "/" + index + "/_doc/" + documentId, null); + if (response == null || response.isBlank()) { + return null; + } + JsonNode node = getObjectMapper().readTree(response); + if (!node.path("found").asBoolean(false)) { + return null; + } + return node; + } catch (IOException e) { + // HttpUtils throws on HTTP 404 when the document is missing + return null; + } + } + + private JsonNode findByItemId(String index, String itemId) throws Exception { + // After migrate reindex, string fields use the folding text mapping with a .keyword subfield. + // Term/prefix on the analyzed field miss camelCase ids such as booleanCondition. + String body = "{\"size\":20,\"query\":{\"bool\":{\"should\":[" + + "{\"term\":{\"itemId.keyword\":\"" + itemId + "\"}}," + + "{\"prefix\":{\"itemId.keyword\":\"" + itemId + "_\"}}," + + "{\"term\":{\"itemId\":\"" + itemId + "\"}}," + + "{\"prefix\":{\"itemId\":\"" + itemId + "_\"}}" + + "],\"minimum_should_match\":1}}}"; + JsonNode hits = getObjectMapper().readTree(HttpUtils.executePostRequest(httpClient, + getSearchEngineBaseUrl() + "/" + index + "/_search", body, null)) + .path("hits").path("hits"); + for (JsonNode hit : hits) { + String found = hit.path("_source").path("itemId").asText(); + String docId = hit.path("_id").asText(); + if (found.equals(itemId) || found.startsWith(itemId + "_") + || docId.equals(itemId) || docId.contains("_" + itemId + "_") + || docId.endsWith("_" + itemId)) { + return hit; + } + } + return null; + } +} diff --git a/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-10-tenantInitialization.groovy b/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-10-tenantInitialization.groovy index dada0f0f4..2cb53fdac 100644 --- a/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-10-tenantInitialization.groovy +++ b/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-10-tenantInitialization.groovy @@ -107,10 +107,10 @@ ${sep} Generated : ${isoDate} Tenant ID : ${tenantId} -PUBLIC KEY (X-Unomi-Public-Key header -- context.json / event collector): +PUBLIC KEY (X-Unomi-Api-Key header -- context.json / event collector): ${generatedPublicKey} -PRIVATE KEY (X-Unomi-Key header -- protected / admin endpoints): +PRIVATE KEY (HTTP Basic authentication as tenantId:privateKey -- protected / admin endpoints): ${generatedPrivateKey} IMPORTANT: These keys cannot be recovered after this file is deleted. From fbaff8fde9a6906f41f098d43d37f7019d1a6273 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Wed, 7 Oct 2026 16:21:26 +0200 Subject: [PATCH 2/6] UNOMI-998: Fix CI suite when Migrate30 runs before Migrate16x Clear open context-* indices before the 1.6 snapshot restore, and mirror OpenSearch connection settings into the elasticsearch.* keys that migration.cfg uses so unomi:migrate does not hit localhost:9200. --- .../Migrate16xToCurrentVersionIT.java | 23 +++++++++++++++++++ .../persistence/OpenSearchITBackend.java | 11 ++++++++- .../resources/org.apache.unomi.migration.cfg | 4 +++- 3 files changed, 36 insertions(+), 2 deletions(-) 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 63e0755ef..f1e16a494 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 @@ -125,6 +125,10 @@ public void waitForStartup() throws InterruptedException { // Restore snapshot from 1.6.x try (CloseableHttpClient httpClient = createSearchEngineHttpClient()) { + // A prior migration IT (e.g. Migrate30) may have left open context-* indices. + // Snapshot restore refuses to overwrite open indices, so clear them first. + deleteExistingContextIndices(httpClient); + // Create snapshot repo HttpUtils.executePutRequest(httpClient, getEsSnapshotRepo(), resourceAsString(RESOURCE_CREATE_SNAPSHOTS_REPO), null); // Get snapshot, insure it exists @@ -191,6 +195,25 @@ public void cleanup() throws InterruptedException { } } + private void deleteExistingContextIndices(CloseableHttpClient httpClient) throws Exception { + String indicesJson = HttpUtils.executeGetRequest(httpClient, + getEsBaseUrl() + "/_cat/indices?h=index&format=json", null); + if (indicesJson == null || indicesJson.isBlank() || "[]".equals(indicesJson.trim())) { + return; + } + JsonNode indices = getObjectMapper().readTree(indicesJson); + if (!indices.isArray()) { + return; + } + for (JsonNode index : indices) { + String indexName = index.path("index").asText(null); + if (indexName != null && indexName.startsWith(INDEX_PREFIX_CONTEXT)) { + LOGGER.info("Deleting existing index before 1.6 snapshot restore: {}", indexName); + MigrationUtils.deleteIndex(httpClient, getEsBaseUrl(), indexName); + } + } + } + /** * Test that validates migrated data from 1.6.x snapshot. * 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 index 04e182264..c7899ae2b 100644 --- a/itests/src/test/java/org/apache/unomi/itests/persistence/OpenSearchITBackend.java +++ b/itests/src/test/java/org/apache/unomi/itests/persistence/OpenSearchITBackend.java @@ -82,15 +82,24 @@ public Option[] featureOptions() { @Override public Option[] configurationOptions() { String port = searchPort(); + String address = "localhost:" + port; 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.addresses", address), 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"), + // org.apache.unomi.migration.cfg still resolves connection settings from the + // elasticsearch.* placeholders. Mirror OpenSearch here so unomi:migrate (e.g. + // Migrate30ToCurrentVersionIT) does not fall back to localhost:9200. + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.addresses", address), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.username", OS_USER), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.password", OS_PASSWORD), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.sslEnable", "false"), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.sslTrustAllCertificates", "true"), }; } diff --git a/tools/shell-commands/src/main/resources/org.apache.unomi.migration.cfg b/tools/shell-commands/src/main/resources/org.apache.unomi.migration.cfg index ef347295a..7b0138416 100644 --- a/tools/shell-commands/src/main/resources/org.apache.unomi.migration.cfg +++ b/tools/shell-commands/src/main/resources/org.apache.unomi.migration.cfg @@ -17,7 +17,9 @@ # Migration config used for silent migration -# Various configuration related to ElasticSearch communication to be able to connect and migrate the data +# Various configuration related to search-engine communication to be able to connect and migrate the data. +# Uses the elasticsearch.* system properties for historical reasons; OpenSearch ITs mirror their +# connection settings into those keys so unomi:migrate reaches the right host/port. esAddresses = ${org.apache.unomi.elasticsearch.addresses:-localhost:9200} esSSLEnabled = ${org.apache.unomi.elasticsearch.sslEnable:-false} esLogin = ${org.apache.unomi.elasticsearch.username:-} From 1e48f5b1e7d39760d45f2d4edfa5609d2a06a3bd Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Wed, 7 Oct 2026 17:10:20 +0200 Subject: [PATCH 3/6] UNOMI-998: Harden the 3.0 migration IT after review Clear the migration history between migration ITs so the 1.6 run no longer skips the 4.0.0 steps, and check what the migration itself wrote before Unomi starts and redeploys definitions and the test tenant. Co-Authored-By: Claude Opus 5.5 --- .../Migrate16xToCurrentVersionIT.java | 28 ++----- .../Migrate30ToCurrentVersionIT.java | 81 ++++++++++++------- .../persistence/OpenSearchITBackend.java | 1 + .../resources/org.apache.unomi.migration.cfg | 4 +- 4 files changed, 62 insertions(+), 52 deletions(-) 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 f1e16a494..aa325edf4 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 @@ -40,6 +40,8 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.*; @Category(SearchBackendIT.class) @@ -127,7 +129,12 @@ public void waitForStartup() throws InterruptedException { try (CloseableHttpClient httpClient = createSearchEngineHttpClient()) { // A prior migration IT (e.g. Migrate30) may have left open context-* indices. // Snapshot restore refuses to overwrite open indices, so clear them first. - deleteExistingContextIndices(httpClient); + for (String index : MigrationUtils.getIndexesPrefixedBy(httpClient, getEsBaseUrl(), INDEX_PREFIX_CONTEXT)) { + LOGGER.info("Deleting existing index before 1.6 snapshot restore: {}", index); + MigrationUtils.deleteIndex(httpClient, getEsBaseUrl(), index); + } + // Its completed history would make this migration skip every 4.0.0 step. + Files.deleteIfExists(Paths.get(karafData(), "migration", "history.json")); // Create snapshot repo HttpUtils.executePutRequest(httpClient, getEsSnapshotRepo(), resourceAsString(RESOURCE_CREATE_SNAPSHOTS_REPO), null); @@ -195,25 +202,6 @@ public void cleanup() throws InterruptedException { } } - private void deleteExistingContextIndices(CloseableHttpClient httpClient) throws Exception { - String indicesJson = HttpUtils.executeGetRequest(httpClient, - getEsBaseUrl() + "/_cat/indices?h=index&format=json", null); - if (indicesJson == null || indicesJson.isBlank() || "[]".equals(indicesJson.trim())) { - return; - } - JsonNode indices = getObjectMapper().readTree(indicesJson); - if (!indices.isArray()) { - return; - } - for (JsonNode index : indices) { - String indexName = index.path("index").asText(null); - if (indexName != null && indexName.startsWith(INDEX_PREFIX_CONTEXT)) { - LOGGER.info("Deleting existing index before 1.6 snapshot restore: {}", indexName); - MigrationUtils.deleteIndex(httpClient, getEsBaseUrl(), indexName); - } - } - } - /** * Test that validates migrated data from 1.6.x snapshot. * diff --git a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate30ToCurrentVersionIT.java b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate30ToCurrentVersionIT.java index 11fe95356..8c93e3354 100644 --- a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate30ToCurrentVersionIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate30ToCurrentVersionIT.java @@ -22,8 +22,10 @@ import org.apache.unomi.api.tenants.Tenant; import org.apache.unomi.itests.BaseIT; import org.apache.unomi.itests.persistence.SearchBackendIT; +import org.apache.unomi.shell.migration.utils.HttpRequestException; 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; @@ -34,7 +36,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryStream; import java.nio.file.Files; @@ -46,8 +47,8 @@ * UNOMI-998: migrate a synthetic Unomi 3.0 data set with {@code unomi:migrate 3.0.0} * before Unomi starts, then assert the post-migration shape that 996/997 fixed. *

- * Must run before {@link Migrate16xToCurrentVersionIT} so that the 1.6 snapshot restore - * can wipe this fixture afterward. Uses HTTP seeding so both Elasticsearch and OpenSearch + * Must run before {@link Migrate16xToCurrentVersionIT}, which deletes this fixture and the + * migration history before restoring its 1.6 snapshot. Uses HTTP seeding so both Elasticsearch and OpenSearch * can run it ({@code httpAdminApi}), unlike the 1.6 snapshot path. *

* One {@code @Test} method only: {@code @Before} must not re-seed on every method, or the @@ -80,12 +81,14 @@ public class Migrate30ToCurrentVersionIT extends BaseIT { private static final String SFDC_ID = "unomi998-sfdc"; private static final String PROFILE_ID = "unomi998-profile"; private static final String OLD_CONDITION_MARKER = "unomi998-3.0-seed-booleanCondition"; + // Post-2.2 document id for system condition types: system__conditiontype + private static final String SHIPPED_CONDITION_DOC_ID = "system_booleanCondition_conditiontype"; private static final String SIMPLE_MAPPING = "{" + "\"settings\":{\"number_of_shards\":1,\"number_of_replicas\":0}," + "\"mappings\":{\"properties\":{" + "\"itemId\":{\"type\":\"keyword\"}," - + "\"itemType\":{\"type\":\"keyword\"}," + + "\"itemType\":{\"type\":\"text\",\"fields\":{\"keyword\":{\"type\":\"keyword\"}}}," + "\"scope\":{\"type\":\"keyword\"}," + "\"tenantId\":{\"type\":\"keyword\"}," + "\"metadata\":{\"type\":\"object\",\"enabled\":true}," @@ -109,6 +112,9 @@ public void waitForStartup() throws InterruptedException { try { httpClient = createSearchEngineHttpClient(); + // A completed history from an earlier run would make this migrate skip every step + Path history = Paths.get(karafData(), "migration", "history.json"); + Files.deleteIfExists(history); seedUnomi30Dataset(httpClient); assertMigrationBundleShipsEngineMappings(); @@ -118,14 +124,13 @@ public void waitForStartup() throws InterruptedException { System.out.println("Migration command output results:"); System.out.println(migrationOutput); Assert.assertNotNull(migrationOutput); - Assert.assertFalse("migration must not abort", - migrationOutput.toLowerCase().contains("migration process aborted")); Assert.assertTrue("migration must run 4.0.0 tenant scripts", migrationOutput.contains("tenantDocumentIds")); - Assert.assertTrue("migration must finish", - migrationOutput.contains("Finish execution of:") - || migrationOutput.contains("migrationStatus")); + // Only written once every script has finished without error + Assert.assertEquals("migration must finish", "COMPLETED", + getObjectMapper().readTree(history.toFile()).path("migrationStatus").asText()); + assertMigrationWroteSystemDefinitionAndTenant(); prepareSearchEngineAfterMigration(); } catch (Throwable t) { LOGGER.error("Error during 3.0 migration setup", t); @@ -135,6 +140,13 @@ public void waitForStartup() throws InterruptedException { super.waitForStartup(); } + @After + public void closeHttpClient() throws Exception { + if (httpClient != null) { + httpClient.close(); + } + } + @Test public void checkMigratedDataFrom30() throws Exception { Assume.assumeTrue( @@ -149,6 +161,24 @@ public void checkMigratedDataFrom30() throws Exception { assertApiKeyFileNamesCorrectHeaders(); } + /** + * Checked before Unomi starts: startup deploys the bundled definitions and BaseIT creates the + * test tenant when it is missing, so afterwards both would exist whatever the migration did. + */ + private void assertMigrationWroteSystemDefinitionAndTenant() throws Exception { + HttpUtils.executePostRequest(httpClient, getSearchEngineBaseUrl() + "/_refresh", null, null); + + JsonNode shipped = getByDocumentId(INDEX_SYSTEMITEMS, SHIPPED_CONDITION_DOC_ID); + Assert.assertNotNull("migration must write " + SHIPPED_CONDITION_DOC_ID, shipped); + Assert.assertEquals("shipped definitions go to system", "system", + shipped.path("_source").path("tenantId").asText()); + + JsonNode tenants = getObjectMapper().readTree(HttpUtils.executeGetRequest(httpClient, + getSearchEngineBaseUrl() + "/" + INDEX_TENANT + "/_search", null)).path("hits").path("hits"); + Assert.assertEquals("migration must write the default tenant", 1, tenants.size()); + Assert.assertEquals(TEST_TENANT_ID, tenants.get(0).path("_source").path("itemId").asText()); + } + private void assertDefaultTenantExists() throws Exception { Assert.assertTrue("tenant index must exist", MigrationUtils.indexExists(httpClient, getSearchEngineBaseUrl(), INDEX_TENANT)); @@ -198,15 +228,11 @@ private void assertEverySeededDocumentHasATenant() throws Exception { if (!MigrationUtils.indexExists(httpClient, getSearchEngineBaseUrl(), index)) { continue; } - JsonNode hits = getObjectMapper().readTree(HttpUtils.executeGetRequest(httpClient, - getSearchEngineBaseUrl() + "/" + index + "/_search?size=100", null)) + JsonNode hits = getObjectMapper().readTree(HttpUtils.executePostRequest(httpClient, + getSearchEngineBaseUrl() + "/" + index + "/_search", + "{\"size\":10,\"_source\":false,\"query\":{\"bool\":{\"must_not\":{\"exists\":{\"field\":\"tenantId\"}}}}}", null)) .path("hits").path("hits"); - Assert.assertTrue(index + " should have documents", hits.size() > 0); - for (JsonNode hit : hits) { - Assert.assertTrue(index + " document " + hit.path("_id").asText() + " must have tenantId", - hit.path("_source").hasNonNull("tenantId") - && !hit.path("_source").path("tenantId").asText().isBlank()); - } + Assert.assertEquals(index + " has documents without tenantId: " + hits, 0, hits.size()); } } @@ -217,17 +243,6 @@ private void assertShippedDefinitionsAreFourZero() throws Exception { ? booleanCondition.getMetadata().getDescription() : null; Assert.assertNotEquals("3.0 seeded marker must not win over the 4.0 bundle definition", OLD_CONDITION_MARKER, description); - - // Post-2.2 document id for system condition types: system__conditiontype - JsonNode migratedShipped = getByDocumentId(INDEX_SYSTEMITEMS, "system_booleanCondition_conditiontype"); - if (migratedShipped == null) { - migratedShipped = findByItemId(INDEX_SYSTEMITEMS, "booleanCondition"); - } - Assert.assertNotNull("booleanCondition must exist in systemitems after migration", migratedShipped); - Assert.assertEquals("shipped definitions go to system", "system", - migratedShipped.path("_source").path("tenantId").asText()); - Assert.assertTrue("shipped definition document id must use the system tenant prefix", - migratedShipped.path("_id").asText().startsWith("system_")); } private void assertApiKeyFileNamesCorrectHeaders() throws Exception { @@ -259,6 +274,8 @@ private void seedUnomi30Dataset(CloseableHttpClient client) throws Exception { System.out.println("Seeding synthetic Unomi 3.0 dataset..."); LOGGER.info("Seeding synthetic Unomi 3.0 dataset..."); + // The migration only creates the tenant (and its key file) when the tenant index is missing + MigrationUtils.deleteIndex(client, es, INDEX_TENANT); for (String index : Arrays.asList(INDEX_SYSTEMITEMS, INDEX_PROFILE, INDEX_GEONAME, INDEX_CLUSTERNODE, INDEX_SFDC)) { MigrationUtils.deleteIndex(client, es, index); MigrationUtils.createIndex(client, es, index, SIMPLE_MAPPING); @@ -290,7 +307,8 @@ private void assertMigrationBundleShipsEngineMappings() { Bundle migrationBundle = FrameworkUtil.getBundle(MigrationUtils.class); Assert.assertNotNull(migrationBundle); for (String engine : Arrays.asList("elasticsearch", "opensearch")) { - for (String mappingFile : Arrays.asList("clusterNode.json", "profile.json", "geonameEntry.json", "systemItems.json")) { + for (String mappingFile : Arrays.asList("clusterNode.json", "event.json", "geonameEntry.json", + "personaSession.json", "profile.json", "profileAlias.json", "session.json", "systemItems.json", "tenant.json")) { String path = "META-INF/cxs/migration-mappings/" + engine + "/" + mappingFile; Assert.assertNotNull("boot migrate needs " + path + " in shell-commands", migrationBundle.getEntry(path)); @@ -315,8 +333,11 @@ private JsonNode getByDocumentId(String index, String documentId) throws Excepti return null; } return node; - } catch (IOException e) { + } catch (HttpRequestException e) { // HttpUtils throws on HTTP 404 when the document is missing + if (e.getCode() != 404) { + throw e; + } return 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 index c7899ae2b..4c225602f 100644 --- a/itests/src/test/java/org/apache/unomi/itests/persistence/OpenSearchITBackend.java +++ b/itests/src/test/java/org/apache/unomi/itests/persistence/OpenSearchITBackend.java @@ -100,6 +100,7 @@ public Option[] configurationOptions() { editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.password", OS_PASSWORD), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.sslEnable", "false"), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.sslTrustAllCertificates", "true"), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.rollover.maxDocs", "300"), }; } diff --git a/tools/shell-commands/src/main/resources/org.apache.unomi.migration.cfg b/tools/shell-commands/src/main/resources/org.apache.unomi.migration.cfg index 7b0138416..62423a6af 100644 --- a/tools/shell-commands/src/main/resources/org.apache.unomi.migration.cfg +++ b/tools/shell-commands/src/main/resources/org.apache.unomi.migration.cfg @@ -18,8 +18,8 @@ # Migration config used for silent migration # Various configuration related to search-engine communication to be able to connect and migrate the data. -# Uses the elasticsearch.* system properties for historical reasons; OpenSearch ITs mirror their -# connection settings into those keys so unomi:migrate reaches the right host/port. +# These keys read the org.apache.unomi.elasticsearch.* system properties for historical reasons, also when +# the search engine is OpenSearch: set them to your OpenSearch connection settings before running unomi:migrate. esAddresses = ${org.apache.unomi.elasticsearch.addresses:-localhost:9200} esSSLEnabled = ${org.apache.unomi.elasticsearch.sslEnable:-false} esLogin = ${org.apache.unomi.elasticsearch.username:-} From 648d6dad710c862192be1df41574f0a573414a2e Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Wed, 7 Oct 2026 19:29:33 +0200 Subject: [PATCH 4/6] UNOMI-998: Recognise the index when the migration reindexes from its clone The reindex script sees the "-cloned" source index in ctx._index, so the systemitems and geonameentry checks never matched during a real migration. Strip the suffix before testing the index name. Co-Authored-By: Claude Opus 5.5 --- .../4.0.0/initialize_tenant_and_audit_fields.painless | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tools/shell-commands/src/main/resources/requestBody/4.0.0/initialize_tenant_and_audit_fields.painless b/tools/shell-commands/src/main/resources/requestBody/4.0.0/initialize_tenant_and_audit_fields.painless index 97dcd9b38..455735b03 100644 --- a/tools/shell-commands/src/main/resources/requestBody/4.0.0/initialize_tenant_and_audit_fields.painless +++ b/tools/shell-commands/src/main/resources/requestBody/4.0.0/initialize_tenant_and_audit_fields.painless @@ -20,12 +20,18 @@ // stay in the configured tenant even when their 3.0 scope is "systemscope". // A shipped definition is recognised by item type and id only, so a 3.0 item sharing both // (such as a built-in rule edited in place) also goes to system. +// The reindex reads from the "-cloned" backup and ctx._index is that source index, so drop the suffix. +def indexLower = ctx._index != null ? ctx._index.toLowerCase(Locale.ROOT) : ''; +if (indexLower.endsWith('-cloned')) { + indexLower = indexLower.substring(0, indexLower.length() - '-cloned'.length()); +} + boolean useSystemTenant = false; if (params.forceDefaultTenant != true) { def itemTypeLower = ctx._source.itemType != null ? ctx._source.itemType.toLowerCase(Locale.ROOT) : ''; // Shipped ids are grouped by item type, so other types (profiles, events...) skip the lookup def shippedIds = params.shippedDefinitionIds != null ? params.shippedDefinitionIds.get(itemTypeLower) : null; - if (itemTypeLower == 'geonameentry' || (ctx._index != null && ctx._index.toLowerCase(Locale.ROOT).endsWith('-geonameentry'))) { + if (itemTypeLower == 'geonameentry' || indexLower.endsWith('-geonameentry')) { useSystemTenant = true; } else if (shippedIds != null) { def rawId = ctx._source.itemId != null ? ctx._source.itemId : ctx._id; @@ -49,7 +55,7 @@ if (ctx._id != null) { // Skip if already has tenant prefix if (!ctx._id.startsWith(params.tenantId + '_') && !ctx._id.startsWith(params.systemTenantId + '_')) { def baseId = ctx._id; - if (ctx._index != null && ctx._index.toLowerCase(Locale.ROOT).endsWith('-systemitems')) { + if (indexLower.endsWith('-systemitems')) { // For system items the document ID is tenantId_itemId_itemtype (see getDocumentIDForItemType). // 3.0 document IDs already carry the lower-case _itemtype suffix, so only add it when missing. def itemType = ctx._source.itemType != null ? ctx._source.itemType.toLowerCase(Locale.ROOT) : null; @@ -69,7 +75,6 @@ if (ctx._id != null) { // systemitems must get audit here: UNOMI-997 puts shipped defs under the system // tenant before Unomi starts, so bundle deploy treats them as updates and would // otherwise leave createdBy unset on docs that never had it. -def indexLower = ctx._index != null ? ctx._index.toLowerCase(Locale.ROOT) : ''; if (!indexLower.endsWith('-geonameentry') && !indexLower.endsWith('-geonames')) { // Handle creation date based on item type if (ctx._source.creationDate == null) { From d67ac7e198485c99a6ee0cf2e7fa35a316db66fd Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Thu, 8 Oct 2026 07:12:22 +0200 Subject: [PATCH 5/6] UNOMI-998: Leave Unomi stopped before the 1.6 snapshot migration Started by the 3.0 migration test, Unomi kept running on indices that the 1.6 test then deletes and restores, and never redeployed its definitions and schemas. Where the snapshot migration runs next, the 3.0 test now only checks the migrated indices. Co-Authored-By: Claude Opus 5.5 --- .../migration/Migrate30ToCurrentVersionIT.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate30ToCurrentVersionIT.java b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate30ToCurrentVersionIT.java index 8c93e3354..6137b73a6 100644 --- a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate30ToCurrentVersionIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate30ToCurrentVersionIT.java @@ -48,7 +48,8 @@ * before Unomi starts, then assert the post-migration shape that 996/997 fixed. *

* Must run before {@link Migrate16xToCurrentVersionIT}, which deletes this fixture and the - * migration history before restoring its 1.6 snapshot. Uses HTTP seeding so both Elasticsearch and OpenSearch + * migration history before restoring its 1.6 snapshot. Where that snapshot migration runs + * (Elasticsearch), this test leaves Unomi stopped and only checks the migrated indices. Uses HTTP seeding so both Elasticsearch and OpenSearch * can run it ({@code httpAdminApi}), unlike the 1.6 snapshot path. *

* One {@code @Test} method only: {@code @Before} must not re-seed on every method, or the @@ -137,7 +138,11 @@ public void waitForStartup() throws InterruptedException { throw new RuntimeException("Error during 3.0 migration setup", t); } - super.waitForStartup(); + // Where the 1.6 snapshot migration runs next, it needs Unomi still stopped: started here, + // Unomi would keep running on indices that test deletes and never redeploy its definitions. + if (!persistenceCapabilities().snapshotRestoreMigration()) { + super.waitForStartup(); + } } @After @@ -153,12 +158,14 @@ public void checkMigratedDataFrom30() throws Exception { "HTTP admin API required (provider=" + getPersistenceBackend().providerId() + ")", persistenceCapabilities().httpAdminApi()); - assertDefaultTenantExists(); assertUserContentStaysOutOfSystemTenant(); assertGeonamesClusterNodeAndSfdc(); assertEverySeededDocumentHasATenant(); - assertShippedDefinitionsAreFourZero(); assertApiKeyFileNamesCorrectHeaders(); + if (unomiStarted) { + assertDefaultTenantExists(); + assertShippedDefinitionsAreFourZero(); + } } /** From f520ecf27fbcdc11d55245751ae027a61bf64e7c Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Thu, 8 Oct 2026 07:31:22 +0200 Subject: [PATCH 6/6] UNOMI-998: Upgrade the Develocity Maven extension to 2.6.0 GitHub's runner image now ships Maven 3.10.0, on which extension 2.5.0 fails with an internal error before the build starts. Co-Authored-By: Claude Opus 5.5 --- .mvn/extensions.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 29ae19e1a..aa08819bc 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -23,7 +23,7 @@ com.gradle develocity-maven-extension - 2.5.0 + 2.6.0 com.gradle