This is an automated email from the ASF dual-hosted git repository. sergehuber pushed a commit to branch UNOMI-996-migration-mappings in repository https://gitbox.apache.org/repos/asf/unomi.git
commit dd5d4954a268d0c4aae1d0224a2fde5ac120d913 Author: Serge Huber <[email protected]> AuthorDate: Tue Oct 6 15:56:16 2026 +0200 UNOMI-996: harden 4.0 tenant document-id migration and mapping IT --- .../test/java/org/apache/unomi/itests/AllITs.java | 2 + .../apache/unomi/itests/CorePersistenceITs.java | 2 + .../itests/migration/Migrate40IndexMappingIT.java | 143 +++++++++++++++++++++ tools/shell-commands/pom.xml | 24 ++++ .../shell/migration/utils/MigrationUtils.java | 99 +++++++++++++- .../migrate-4.0.0-01-tenantDocumentIds.groovy | 26 ++-- .../initialize_tenant_and_audit_fields.painless | 10 +- .../shell/migration/utils/MigrationUtilsTest.java | 67 ++++++++++ 8 files changed, 354 insertions(+), 19 deletions(-) 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 61e39077e..4d377c022 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 @@ package org.apache.unomi.itests; import org.apache.unomi.itests.migration.Migrate16xToCurrentVersionIT; import org.apache.unomi.itests.graphql.*; +import org.apache.unomi.itests.migration.Migrate40IndexMappingIT; import org.apache.unomi.itests.migration.MigrationIT; import org.apache.unomi.itests.shell.*; import org.junit.runner.RunWith; @@ -34,6 +35,7 @@ import org.junit.runners.Suite.SuiteClasses; @SuiteClasses({ Migrate16xToCurrentVersionIT.class, MigrationIT.class, + Migrate40IndexMappingIT.class, PersistenceServiceIT.class, BasicIT.class, ConditionEvaluatorIT.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 3d19d4538..bf50ac2b2 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 @@ package org.apache.unomi.itests; import org.apache.unomi.itests.migration.Migrate16xToCurrentVersionIT; import org.apache.unomi.itests.graphql.*; +import org.apache.unomi.itests.migration.Migrate40IndexMappingIT; import org.apache.unomi.itests.migration.MigrationIT; import org.apache.unomi.itests.shell.*; import org.junit.runner.RunWith; @@ -35,6 +36,7 @@ import org.junit.runners.Suite.SuiteClasses; @SuiteClasses({ Migrate16xToCurrentVersionIT.class, MigrationIT.class, + Migrate40IndexMappingIT.class, PersistenceServiceIT.class, BasicIT.class, ConditionEvaluatorIT.class, diff --git a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate40IndexMappingIT.java b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate40IndexMappingIT.java new file mode 100644 index 000000000..884b3d634 --- /dev/null +++ b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate40IndexMappingIT.java @@ -0,0 +1,143 @@ +/* + * 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 org.apache.http.impl.client.CloseableHttpClient; +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.json.JSONArray; +import org.json.JSONObject; +import org.junit.After; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.osgi.framework.FrameworkUtil; + +import java.util.Arrays; + +/** + * UNOMI-996: migration must find bundled mappings (including cluster nodes) and + * copy the mapping of an unknown index instead of stopping. + */ +@Category(SearchBackendIT.class) +public class Migrate40IndexMappingIT extends BaseIT { + + private static final String UNKNOWN_INDEX = "context-unomi996unknown"; + private static final String DEST_INDEX = "context-unomi996unknown-dest"; + + @After + public void deleteTestIndices() throws Exception { + if (!persistenceCapabilities().httpAdminApi()) { + return; + } + try (CloseableHttpClient httpClient = createSearchEngineHttpClient()) { + String es = getSearchEngineBaseUrl(); + MigrationUtils.deleteIndex(httpClient, es, UNKNOWN_INDEX); + MigrationUtils.deleteIndex(httpClient, es, DEST_INDEX); + } + } + + @Test + public void clusterNodeMappingIsBundledWithTheMigrationCommand() throws Exception { + String mapping = MigrationUtils.extractMappingFromBundles( + FrameworkUtil.getBundle(MigrationUtils.class).getBundleContext(), "clusterNode.json"); + Assert.assertTrue("clusterNode mapping should define cpuLoad", mapping.contains("cpuLoad")); + } + + @Test + public void unknownIndexKeepsDocumentsOnTheConfiguredTenant() throws Exception { + Assume.assumeTrue( + "HTTP admin API required to create migration fixture indices (provider=" + + getPersistenceBackend().providerId() + ")", + persistenceCapabilities().httpAdminApi()); + + try (CloseableHttpClient httpClient = createSearchEngineHttpClient()) { + String es = getSearchEngineBaseUrl(); + org.osgi.framework.BundleContext migrationBundles = + FrameworkUtil.getBundle(MigrationUtils.class).getBundleContext(); + + String sourceSettings = "{" + + "\"settings\":{\"number_of_shards\":1,\"number_of_replicas\":0}," + + "\"mappings\":{\"properties\":{" + + "\"itemId\":{\"type\":\"keyword\"}," + + "\"itemType\":{\"type\":\"keyword\"}," + + "\"scope\":{\"type\":\"keyword\"}," + + "\"tenantId\":{\"type\":\"keyword\"}" + + "}}}"; + HttpUtils.executePutRequest(httpClient, es + "/" + UNKNOWN_INDEX, sourceSettings, null); + HttpUtils.executePutRequest(httpClient, es + "/" + UNKNOWN_INDEX + + "/_doc/unknown1?refresh=true", + "{\"itemId\":\"unknown1\",\"itemType\":\"sfdcConfiguration\"}", null); + HttpUtils.executePutRequest(httpClient, es + "/" + UNKNOWN_INDEX + + "/_doc/unknown2?refresh=true", + "{\"itemId\":\"unknown2\",\"itemType\":\"sfdcConfiguration\",\"scope\":\"system\"}", null); + + String mapping = MigrationUtils.resolveIndexMapping( + migrationBundles, httpClient, es, UNKNOWN_INDEX, "does-not-exist.json"); + Assert.assertTrue("copied mapping should keep itemType", mapping.contains("itemType")); + + Assert.assertEquals("sfdcconfiguration", + MigrationUtils.resolveItemType(UNKNOWN_INDEX, "context", + Arrays.asList("profile", "event", "clusterNode", "generic"))); + + String destSettings = "{\"settings\":{\"number_of_shards\":1,\"number_of_replicas\":0},\"mappings\":" + + mapping + "}"; + MigrationUtils.createIndex(httpClient, es, DEST_INDEX, destSettings); + + String painless = MigrationUtils.getFileWithoutComments( + migrationBundles, "requestBody/4.0.0/initialize_tenant_and_audit_fields.painless"); + JSONObject paramsObj = new JSONObject(); + paramsObj.put("tenantId", TEST_TENANT_ID); + paramsObj.put("systemTenantId", "system"); + paramsObj.put("forceDefaultTenant", true); + paramsObj.put("itemType", "generic"); + paramsObj.put("date", "2026-01-01T00:00:00Z"); + paramsObj.put("systemItems", new JSONArray()); + + JSONObject scriptObj = new JSONObject(); + scriptObj.put("source", painless); + scriptObj.put("lang", "painless"); + scriptObj.put("params", paramsObj); + + JSONObject reindex = new JSONObject(); + reindex.put("source", new JSONObject().put("index", UNKNOWN_INDEX)); + reindex.put("dest", new JSONObject().put("index", DEST_INDEX)); + reindex.put("script", scriptObj); + HttpUtils.executePostRequest(httpClient, es + "/_reindex?refresh=true&wait_for_completion=true", + reindex.toString(), null); + + String search = HttpUtils.executeGetRequest(httpClient, + es + "/" + DEST_INDEX + "/_search?size=10", null); + JSONArray hits = new JSONObject(search).getJSONObject("hits").getJSONArray("hits"); + Assert.assertEquals(2, hits.length()); + for (int i = 0; i < hits.length(); i++) { + JSONObject hit = hits.getJSONObject(i); + String id = hit.getString("_id"); + JSONObject source = hit.getJSONObject("_source"); + Assert.assertTrue("document id should use the configured tenant: " + id, + id.startsWith(TEST_TENANT_ID + "_")); + Assert.assertFalse("unknown index documents must not use the system tenant: " + id, + id.startsWith("system_")); + Assert.assertEquals(TEST_TENANT_ID, source.getString("tenantId")); + Assert.assertNotEquals("system", source.getString("tenantId")); + } + } + } +} diff --git a/tools/shell-commands/pom.xml b/tools/shell-commands/pom.xml index 9451764d7..4f0aef63b 100644 --- a/tools/shell-commands/pom.xml +++ b/tools/shell-commands/pom.xml @@ -158,6 +158,30 @@ <build> <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-resources-plugin</artifactId> + <executions> + <execution> + <id>copy-persistence-mappings</id> + <phase>generate-resources</phase> + <goals> + <goal>copy-resources</goal> + </goals> + <configuration> + <outputDirectory>${project.build.outputDirectory}/META-INF/cxs/mappings</outputDirectory> + <resources> + <resource> + <directory>${project.basedir}/../../persistence-elasticsearch/core/src/main/resources/META-INF/cxs/mappings</directory> + </resource> + <resource> + <directory>${project.basedir}/../../extensions/geonames/services/src/main/resources/META-INF/cxs/mappings</directory> + </resource> + </resources> + </configuration> + </execution> + </executions> + </plugin> <plugin> <groupId>org.apache.felix</groupId> <artifactId>maven-bundle-plugin</artifactId> diff --git a/tools/shell-commands/src/main/java/org/apache/unomi/shell/migration/utils/MigrationUtils.java b/tools/shell-commands/src/main/java/org/apache/unomi/shell/migration/utils/MigrationUtils.java index b80055576..f2d33be07 100644 --- a/tools/shell-commands/src/main/java/org/apache/unomi/shell/migration/utils/MigrationUtils.java +++ b/tools/shell-commands/src/main/java/org/apache/unomi/shell/migration/utils/MigrationUtils.java @@ -303,6 +303,24 @@ public class MigrationUtils { } public static String extractMappingFromBundles(BundleContext bundleContext, String fileName) throws IOException { + String mapping = findMappingInBundles(bundleContext, fileName); + if (mapping == null) { + throw new RuntimeException("no mapping found in bundles for: " + fileName); + } + return mapping; + } + + /** + * Looks up {@code META-INF/cxs/mappings/{fileName}} in every OSGi bundle. + * The migration command bundle ships a copy of the persistence mappings so this + * works before Unomi persistence features are started. + * + * @return the mapping JSON, or {@code null} if no bundle contains the file + */ + public static String findMappingInBundles(BundleContext bundleContext, String fileName) throws IOException { + if (bundleContext == null || fileName == null || fileName.isBlank()) { + return null; + } for (Bundle bundle : bundleContext.getBundles()) { Enumeration<URL> predefinedMappings = bundle.findEntries("META-INF/cxs/mappings", fileName, true); if (predefinedMappings == null) { @@ -310,11 +328,88 @@ public class MigrationUtils { } if (predefinedMappings.hasMoreElements()) { URL predefinedMappingURL = predefinedMappings.nextElement(); - return IOUtils.toString(predefinedMappingURL); + return IOUtils.toString(predefinedMappingURL, StandardCharsets.UTF_8); } } + return null; + } + + /** + * Resolves the mapping used to recreate an index during migration. + * Prefers a bundled file when present (the 4.0 definition). Otherwise copies + * the mapping currently stored on the index, so unknown types do not stop the run. + */ + public static String resolveIndexMapping(BundleContext bundleContext, CloseableHttpClient httpClient, + String esAddress, String indexName, String mappingFileName) throws IOException { + if (mappingFileName != null && !mappingFileName.isBlank()) { + String bundled = findMappingInBundles(bundleContext, mappingFileName); + if (bundled != null) { + return bundled; + } + } + return extractMappingFromIndex(httpClient, esAddress, indexName); + } + + public static String extractMappingFromIndex(CloseableHttpClient httpClient, String esAddress, String indexName) throws IOException { + if (indexName == null || indexName.isBlank()) { + throw new IllegalArgumentException("An index name is required to copy its mapping"); + } + String response = HttpUtils.executeGetRequest(httpClient, esAddress + "/" + indexName + "/_mapping", null); + return mappingsFromIndexResponse(response); + } + + /** + * Reads the mappings object from a GET {@code /{index}/_mapping} body. + */ + public static String mappingsFromIndexResponse(String responseBody) { + if (responseBody == null || responseBody.isBlank()) { + throw new IllegalArgumentException("Index mapping response is empty"); + } + JSONObject root = new JSONObject(responseBody); + if (root.keySet().isEmpty()) { + throw new IllegalArgumentException("Index mapping response has no indices"); + } + String firstIndex = root.keys().next(); + JSONObject index = root.getJSONObject(firstIndex); + if (!index.has("mappings")) { + throw new IllegalArgumentException("Index mapping response has no mappings for " + firstIndex); + } + return index.getJSONObject("mappings").toString(); + } - throw new RuntimeException("no mapping found in bundles for: " + fileName); + /** + * Maps an index name such as {@code context-event-000001} or {@code context-clusternode} + * to an item type. Known types are matched longest-first, ignoring case. + * Unknown names keep the suffix after the prefix (without a rollover number). + */ + public static String resolveItemType(String indexName, String indexPrefix, Collection<String> knownTypes) { + if (indexName == null || indexName.isBlank()) { + throw new IllegalArgumentException("An index name is required"); + } + if (indexPrefix == null || indexPrefix.isBlank()) { + throw new IllegalArgumentException("An index prefix is required"); + } + String lowerIndex = indexName.toLowerCase(Locale.ROOT); + String prefix = indexPrefix.toLowerCase(Locale.ROOT) + "-"; + if (knownTypes != null) { + List<String> types = new ArrayList<>(); + for (String type : knownTypes) { + if (type != null && !type.isBlank() && !"generic".equals(type)) { + types.add(type); + } + } + types.sort((a, b) -> Integer.compare(b.length(), a.length())); + for (String type : types) { + if (lowerIndex.startsWith(prefix + type.toLowerCase(Locale.ROOT))) { + return type; + } + } + } + if (lowerIndex.startsWith(prefix)) { + String rest = indexName.substring(indexPrefix.length() + 1); + return rest.replaceFirst("-\\d{6}$", ""); + } + return "generic"; } public static String buildIndexCreationRequest(String baseIndexSettings, String mapping, MigrationContext context, boolean isMonthlyIndex) throws IOException { diff --git a/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-01-tenantDocumentIds.groovy b/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-01-tenantDocumentIds.groovy index 647aae6ac..b8557b50e 100644 --- a/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-01-tenantDocumentIds.groovy +++ b/tools/shell-commands/src/main/resources/META-INF/cxs/migration/migrate-4.0.0-01-tenantDocumentIds.groovy @@ -67,22 +67,18 @@ def indexConfigs = [ mapping: "personaSession.json", useRollover: false ], + "clusterNode": [ + baseSettings: "requestBody/2.0.0/base_index_mapping.json", + mapping: "clusterNode.json", + useRollover: false + ], "generic": [ baseSettings: "requestBody/2.0.0/base_index_mapping.json", - mapping: null, // Will be determined dynamically from resolved item type + mapping: null, // Copy the mapping currently stored on the index useRollover: false ] ] -// Helper function to resolve item type from index name -def resolveItemType = { String indexName -> - def type = indexConfigs.find { type, config -> - indexName.startsWith("${indexPrefix}-${type}") - } - return type ? type.key : "generic" -} - -// Helper function to get index configuration def getIndexConfig = { String itemType -> return indexConfigs[itemType] ?: indexConfigs["generic"] } @@ -122,18 +118,20 @@ context.performMigrationStep("4.0.0-get-all-indices", () -> { context.printMessage("Processing index: " + indexName) // Determine item type and get configuration - String itemType = resolveItemType(indexName) + String itemType = MigrationUtils.resolveItemType(indexName, indexPrefix, indexConfigs.keySet()) def indexConfig = getIndexConfig(itemType) // Add item type to parameters Map<String, Object> params = new HashMap<>(baseParams) params.put("itemType", itemType) + // Cluster nodes and unknown indices keep site data in the configured tenant, never "system" + boolean forceDefaultTenant = (itemType == "clusterNode" || indexConfig.mapping == null) + params.put("forceDefaultTenant", forceDefaultTenant) // Get base settings and mapping String baseSettings = MigrationUtils.resourceAsString(bundleContext, indexConfig.baseSettings) - String mapping = indexConfig.mapping ? - MigrationUtils.extractMappingFromBundles(bundleContext, indexConfig.mapping) : - MigrationUtils.extractMappingFromBundles(bundleContext, "${itemType}.json") + String mappingFile = indexConfig.mapping ?: "${itemType}.json" + String mapping = MigrationUtils.resolveIndexMapping(bundleContext, context.getHttpClient(), esAddress, indexName, mappingFile) // Build index settings String newIndexSettings 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 07b89377b..7b089252d 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 @@ -18,8 +18,10 @@ if (ctx._id != null) { // Skip if already has tenant prefix if (!ctx._id.startsWith(params.tenantId + '_') && !ctx._id.startsWith(params.systemTenantId + '_')) { - // For geonames or items with system scope, use system tenant - if (ctx._index.endsWith('-geonames') || (ctx._source.scope != null && ctx._source.scope == 'system')) { + // Cluster nodes and unknown indices always stay in the configured (default) tenant + if (params.forceDefaultTenant == true) { + ctx._id = params.tenantId + '_' + ctx._id; + } else if (ctx._index.endsWith('-geonames') || (ctx._source.scope != null && ctx._source.scope == 'system')) { // Preserve the entire original document ID (it may contain underscores, e.g., "dummy_scope") // Do NOT split on underscores - this was a bug in the original migration that caused itemIds // like "dummy_scope" to become "dummy" when the document ID was "dummy_scope_scope" @@ -92,7 +94,9 @@ if (!ctx._index.endsWith('-systemitems') && !ctx._index.endsWith('-geonames')) { // Set tenant ID in the source document based on scope for ALL items if (ctx._source.tenantId == null) { - if (ctx._index.endsWith('-geonames') || (ctx._source.scope != null && ctx._source.scope == 'system')) { + if (params.forceDefaultTenant == true) { + ctx._source.tenantId = params.tenantId; + } else if (ctx._index.endsWith('-geonames') || (ctx._source.scope != null && ctx._source.scope == 'system')) { ctx._source.tenantId = params.systemTenantId; } else { ctx._source.tenantId = params.tenantId; diff --git a/tools/shell-commands/src/test/java/org/apache/unomi/shell/migration/utils/MigrationUtilsTest.java b/tools/shell-commands/src/test/java/org/apache/unomi/shell/migration/utils/MigrationUtilsTest.java index 96129f47c..2be643754 100644 --- a/tools/shell-commands/src/test/java/org/apache/unomi/shell/migration/utils/MigrationUtilsTest.java +++ b/tools/shell-commands/src/test/java/org/apache/unomi/shell/migration/utils/MigrationUtilsTest.java @@ -25,6 +25,11 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; @@ -209,4 +214,66 @@ public class MigrationUtilsTest { () -> MigrationUtils.getFileWithoutComments(bundleContext, "missing.painless")); assertEquals("Resource not found: missing.painless", ex.getMessage()); } + + @Test + public void extractMappingFromBundlesFindsKnownTypeWithoutPersistenceBundle() throws Exception { + Path mappingFile = Files.createTempFile("clusterNode", ".json"); + Files.writeString(mappingFile, "{\"properties\":{\"cpuLoad\":{\"type\":\"double\"}}}"); + URL mappingUrl = mappingFile.toUri().toURL(); + when(bundleContext.getBundles()).thenReturn(new Bundle[]{bundle}); + when(bundle.findEntries("META-INF/cxs/mappings", "clusterNode.json", true)) + .thenReturn(Collections.enumeration(Collections.singletonList(mappingUrl))); + + String mapping = MigrationUtils.extractMappingFromBundles(bundleContext, "clusterNode.json"); + assertEquals("{\"properties\":{\"cpuLoad\":{\"type\":\"double\"}}}", mapping); + Files.deleteIfExists(mappingFile); + } + + @Test + public void extractMappingFromBundlesThrowsWhenMissing() { + when(bundleContext.getBundles()).thenReturn(new Bundle[]{bundle}); + when(bundle.findEntries("META-INF/cxs/mappings", "generic.json", true)).thenReturn(null); + + RuntimeException ex = assertThrows(RuntimeException.class, + () -> MigrationUtils.extractMappingFromBundles(bundleContext, "generic.json")); + assertEquals("no mapping found in bundles for: generic.json", ex.getMessage()); + } + + @Test + public void mappingsFromIndexResponseReadsFirstIndex() { + String body = "{\"context-sfdcconfiguration\":{\"mappings\":{\"properties\":{\"itemId\":{\"type\":\"keyword\"}}}}}"; + String mappings = MigrationUtils.mappingsFromIndexResponse(body); + assertEquals("{\"properties\":{\"itemId\":{\"type\":\"keyword\"}}}", mappings); + } + + @Test + public void mappingsFromIndexResponseRejectsEmptyBody() { + assertThrows(IllegalArgumentException.class, () -> MigrationUtils.mappingsFromIndexResponse("")); + assertThrows(IllegalArgumentException.class, () -> MigrationUtils.mappingsFromIndexResponse("{}")); + } + + @Test + public void resolveItemTypeMatchesClusterNodeIgnoringCase() { + Collection<String> types = Arrays.asList("profile", "event", "clusterNode", "generic"); + assertEquals("clusterNode", MigrationUtils.resolveItemType("context-clusternode", "context", types)); + assertEquals("clusterNode", MigrationUtils.resolveItemType("context-clusterNode", "context", types)); + } + + @Test + public void resolveItemTypeMatchesRolloverEvent() { + Collection<String> types = Arrays.asList("event", "session", "generic"); + assertEquals("event", MigrationUtils.resolveItemType("context-event-000001", "context", types)); + } + + @Test + public void resolveItemTypeKeepsUnknownIndexSuffix() { + Collection<String> types = Arrays.asList("profile", "event", "generic"); + assertEquals("sfdcconfiguration", MigrationUtils.resolveItemType("context-sfdcconfiguration", "context", types)); + } + + @Test + public void resolveItemTypePrefersLongerKnownType() { + Collection<String> types = Arrays.asList("profile", "profileAlias"); + assertEquals("profileAlias", MigrationUtils.resolveItemType("context-profilealias", "context", types)); + } } \ No newline at end of file
