This is an automated email from the ASF dual-hosted git repository. shuber pushed a commit to branch opensearch-persistence in repository https://gitbox.apache.org/repos/asf/unomi.git
commit 16c6de9114fee82110534ea13239cc59daadbee5 Author: Serge Huber <shu...@jahia.com> AuthorDate: Sun Dec 22 10:57:00 2024 +0100 Work on making integration tests work with OpenSearch: - Removed elasticsearch-core from bundle watch requirements - Fix issues with date parsing due to case sensitivity - Improved test units for date parsing and date math handling - Modified HealthChecks to provide an OpenSearch check provider (not yet fully working) - Deactivate 1.x to 2.x migration integration test for OpenSearch (No OpenSearch users will be coming from 1.x) - Update OpenSearch past event query builder to latest changes done in ElasticSearch past event query builder - Various fixes in the integration tests to make them compatible with OpenSearch (removed hardcoded elasticsearch configuration and references) - Added new shell script in itests directory to make it easier to handle the dynamically generated Pax Exam Karaf test container directory. Documentation is also included in the README file inside the itests directory. --- .../unomi/healthcheck/HealthCheckConfig.java | 7 ++- .../provider/ElasticSearchHealthCheckProvider.java | 5 +- ...der.java => OpenSearchHealthCheckProvider.java} | 31 +++++----- .../resources/org.apache.unomi.healthcheck.cfg | 11 +++- itests/README.md | 51 +++++++++++++++++ .../test/java/org/apache/unomi/itests/AllITs.java | 4 +- .../test/java/org/apache/unomi/itests/BaseIT.java | 7 +++ ...BuilderIT.java => ConditionQueryBuilderIT.java} | 4 +- .../org/apache/unomi/itests/HealthCheckIT.java | 54 +----------------- .../org/apache/unomi/itests/ProfileServiceIT.java | 14 ++--- .../itests/ProfileServiceWithoutOverwriteIT.java | 8 ++- .../unomi/itests/migration/Migrate16xTo220IT.java | 17 ++++++ .../resources/org.apache.unomi.healthcheck.cfg | 9 ++- .../resources/OSGI-INF/blueprint/blueprint.xml | 1 - .../PastEventConditionOSQueryBuilder.java | 66 ++++++++++------------ .../resources/META-INF/cxs/mappings/profile.json | 7 +++ .../persistence/spi/conditions/DateUtils.java | 2 +- .../spi/conditions/datemath/DateMathParser.java | 17 +++++- .../spi/conditions/datemath/JavaDateFormatter.java | 12 ++++ .../conditions/datemath/DateMathParserTest.java | 50 ++++++++++++++++ .../conditions/datemath/JavaDateFormatterTest.java | 43 +++++++++++++- 21 files changed, 290 insertions(+), 130 deletions(-) diff --git a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckConfig.java b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckConfig.java index e86018dc3..9cd7a2662 100644 --- a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckConfig.java +++ b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckConfig.java @@ -40,7 +40,12 @@ public class HealthCheckConfig { public static final String CONFIG_ES_SSL_ENABLED = "esSSLEnabled"; public static final String CONFIG_ES_LOGIN = "esLogin"; public static final String CONFIG_ES_PASSWORD = "esPassword"; - public static final String CONFIG_TRUST_ALL_CERTIFICATES = "httpClient.trustAllCertificates"; + public static final String CONFIG_ES_TRUST_ALL_CERTIFICATES = "esHttpClient.trustAllCertificates"; + public static final String CONFIG_OS_ADDRESSES = "osAddresses"; + public static final String CONFIG_OS_SSL_ENABLED = "osSSLEnabled"; + public static final String CONFIG_OS_LOGIN = "osLogin"; + public static final String CONFIG_OS_PASSWORD = "osPassword"; + public static final String CONFIG_OS_TRUST_ALL_CERTIFICATES = "osHttpClient.trustAllCertificates"; public static final String CONFIG_AUTH_REALM = "authentication.realm"; public static final String ENABLED = "healthcheck.enabled"; public static final String PROVIDERS = "healthcheck.providers"; diff --git a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java index 361e68df7..1dc9e146e 100644 --- a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java +++ b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java @@ -26,11 +26,10 @@ import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.apache.unomi.healthcheck.HealthCheckConfig; -import org.apache.unomi.healthcheck.HealthCheckResponse; import org.apache.unomi.healthcheck.HealthCheckProvider; +import org.apache.unomi.healthcheck.HealthCheckResponse; import org.apache.unomi.healthcheck.util.CachedValue; import org.apache.unomi.shell.migration.utils.HttpUtils; import org.osgi.service.component.annotations.Activate; @@ -77,7 +76,7 @@ public class ElasticSearchHealthCheckProvider implements HealthCheckProvider { } try { httpClient = HttpUtils.initHttpClient( - Boolean.parseBoolean(config.get(HealthCheckConfig.CONFIG_TRUST_ALL_CERTIFICATES)), credentialsProvider); + Boolean.parseBoolean(config.get(HealthCheckConfig.CONFIG_ES_TRUST_ALL_CERTIFICATES)), credentialsProvider); } catch (IOException e) { LOGGER.error("Unable to initialize http client", e); } diff --git a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/OpenSearchHealthCheckProvider.java similarity index 82% copy from extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java copy to extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/OpenSearchHealthCheckProvider.java index 361e68df7..3524d1daa 100644 --- a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java +++ b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/OpenSearchHealthCheckProvider.java @@ -26,11 +26,10 @@ import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.apache.unomi.healthcheck.HealthCheckConfig; -import org.apache.unomi.healthcheck.HealthCheckResponse; import org.apache.unomi.healthcheck.HealthCheckProvider; +import org.apache.unomi.healthcheck.HealthCheckResponse; import org.apache.unomi.healthcheck.util.CachedValue; import org.apache.unomi.shell.migration.utils.HttpUtils; import org.osgi.service.component.annotations.Activate; @@ -44,15 +43,15 @@ import java.io.IOException; import java.util.concurrent.TimeUnit; /** - * A Health Check that checks the status of the ElasticSearch connectivity according to the provided configuration. + * A Health Check that checks the status of the OpenSearch connectivity according to the provided configuration. * This connectivity should be LIVE before any try to start Unomi. */ @Component(service = HealthCheckProvider.class, immediate = true) -public class ElasticSearchHealthCheckProvider implements HealthCheckProvider { +public class OpenSearchHealthCheckProvider implements HealthCheckProvider { - public static final String NAME = "elasticsearch"; + public static final String NAME = "opensearch"; - private static final Logger LOGGER = LoggerFactory.getLogger(ElasticSearchHealthCheckProvider.class.getName()); + private static final Logger LOGGER = LoggerFactory.getLogger(OpenSearchHealthCheckProvider.class.getName()); private final CachedValue<HealthCheckResponse> cache = new CachedValue<>(10, TimeUnit.SECONDS); @Reference(cardinality = ReferenceCardinality.MANDATORY) @@ -60,24 +59,24 @@ public class ElasticSearchHealthCheckProvider implements HealthCheckProvider { private CloseableHttpClient httpClient; - public ElasticSearchHealthCheckProvider() { - LOGGER.info("Building elasticsearch health provider service..."); + public OpenSearchHealthCheckProvider() { + LOGGER.info("Building OpenSearch health provider service..."); } @Activate public void activate() { - LOGGER.info("Activating elasticsearch health provider service..."); + LOGGER.info("Activating OpenSearch health provider service..."); CredentialsProvider credentialsProvider = null; - String login = config.get(HealthCheckConfig.CONFIG_ES_LOGIN); + String login = config.get(HealthCheckConfig.CONFIG_OS_LOGIN); // Reuse ElasticSearch credentials key if (StringUtils.isNotEmpty(login)) { credentialsProvider = new BasicCredentialsProvider(); UsernamePasswordCredentials credentials - = new UsernamePasswordCredentials(login, config.get(HealthCheckConfig.CONFIG_ES_PASSWORD)); + = new UsernamePasswordCredentials(login, config.get(HealthCheckConfig.CONFIG_OS_PASSWORD)); credentialsProvider.setCredentials(AuthScope.ANY, credentials); } try { httpClient = HttpUtils.initHttpClient( - Boolean.parseBoolean(config.get(HealthCheckConfig.CONFIG_TRUST_ALL_CERTIFICATES)), credentialsProvider); + Boolean.parseBoolean(config.get(HealthCheckConfig.CONFIG_OS_TRUST_ALL_CERTIFICATES)), credentialsProvider); } catch (IOException e) { LOGGER.error("Unable to initialize http client", e); } @@ -92,7 +91,7 @@ public class ElasticSearchHealthCheckProvider implements HealthCheckProvider { } @Override public HealthCheckResponse execute() { - LOGGER.debug("Health check elasticsearch"); + LOGGER.debug("Health check OpenSearch"); if (cache.isStaled() || cache.getValue().isDown() || cache.getValue().isError()) { cache.setValue(refresh()); } @@ -104,8 +103,8 @@ public class ElasticSearchHealthCheckProvider implements HealthCheckProvider { HealthCheckResponse.Builder builder = new HealthCheckResponse.Builder(); builder.name(NAME).down(); String url = (config.get(HealthCheckConfig.CONFIG_ES_SSL_ENABLED).equals("true") ? "https://" : "http://") - .concat(config.get(HealthCheckConfig.CONFIG_ES_ADDRESSES).split(",")[0].trim()) - .concat("/_cluster/health"); + .concat(config.get(HealthCheckConfig.CONFIG_ES_ADDRESSES).split(",")[0].trim()) + .concat("/_cluster/health"); CloseableHttpResponse response = null; try { response = httpClient.execute(new HttpGet(url)); @@ -119,7 +118,7 @@ public class ElasticSearchHealthCheckProvider implements HealthCheckProvider { } } catch (IOException e) { builder.error().withData("error", e.getMessage()); - LOGGER.error("Error while checking elasticsearch health", e); + LOGGER.error("Error while checking OpenSearch health", e); } finally { if (response != null) { EntityUtils.consumeQuietly(response.getEntity()); diff --git a/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck.cfg b/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck.cfg index 4cbe767eb..b73f7ca4d 100644 --- a/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck.cfg +++ b/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck.cfg @@ -20,12 +20,19 @@ esAddresses = ${org.apache.unomi.elasticsearch.addresses:-localhost:9200} esSSLEnabled = ${org.apache.unomi.elasticsearch.sslEnable:-false} esLogin = ${org.apache.unomi.elasticsearch.username:-} esPassword = ${org.apache.unomi.elasticsearch.password:-} -httpClient.trustAllCertificates = ${org.apache.unomi.elasticsearch.sslTrustAllCertificates:-false} +esHttpClient.trustAllCertificates = ${org.apache.unomi.elasticsearch.sslTrustAllCertificates:-false} + +# OpenSearch configuration +osAddresses = ${org.apache.unomi.opensearch.addresses:-localhost:9200} +osSSLEnabled = ${org.apache.unomi.opensearch.sslEnable:-true} +osLogin = ${org.apache.unomi.opensearch.username:-admin} +osPassword = ${org.apache.unomi.opensearch.password:-} +osHttpClient.trustAllCertificates = ${org.apache.unomi.opensearch.sslTrustAllCertificates:-true} # Security configuration authentication.realm = ${org.apache.unomi.security.realm:-karaf} # Health check configuration healthcheck.enabled = ${org.apache.unomi.healthcheck.enabled:-false} -healthcheck.providers = ${org.apache.unomi.healthcheck.providers:-cluster,elasticsearch,unomi,persistence} +healthcheck.providers = ${org.apache.unomi.healthcheck.providers:-cluster,elasticsearch,opensearch,unomi,persistence} healthcheck.timeout = ${org.apache.unomi.healthcheck.timeout:-400} diff --git a/itests/README.md b/itests/README.md index 85e5aca2d..c24eee02a 100644 --- a/itests/README.md +++ b/itests/README.md @@ -274,3 +274,54 @@ And the final step is, zipping the new version of the snapshot repository and re > In case you are using docker, do zip in the container and use `docker cp` to > get the zip file from the docker container. Now you can modify the migration test class to test that your added data in 1.6.x is correctly migrated in 2.0.0 + +# Integration Tests + +This directory contains the integration tests for Apache Unomi. + +## Karaf Tools + +The `kt.sh` script (short for "Karaf Tools") provides convenient utilities for working with Karaf logs and directories during integration testing. Since Karaf test directories are created with unique UUIDs for each test run, this script helps locate and work with the latest test instance. + +### Usage + +```bash +./kt.sh COMMAND [ARGS] +``` + +### Available Commands + +| Command | Alias | Description | +|-------------|-------|-------------------------------------------------------| +| `log` | `l` | View the latest Karaf log file using less | +| `tail` | `t` | Tail the current Karaf log file | +| `grep` | `g` | Grep the latest Karaf log file (requires pattern) | +| `dir` | `d` | Print the latest Karaf directory path | +| `pushd` | `p` | Change to the latest Karaf directory using pushd | +| `help` | `h` | Show help message | + +### Examples + +```bash +# View log with less +./kt.sh log + +# Tail log file +./kt.sh tail + +# Search for ERROR in log file +./kt.sh grep ERROR + +# Print Karaf directory path +./kt.sh dir + +# Change to Karaf directory +./kt.sh pushd +``` + +### Tips + +- The script automatically finds the most recently created Karaf test directory +- All commands have short aliases (single letter) for faster typing +- Error handling is included for missing directories and files +- The script is particularly useful when debugging integration test failures 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 f415c3ab9..bc6614a17 100644 --- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java +++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java @@ -17,8 +17,8 @@ package org.apache.unomi.itests; -import org.apache.unomi.itests.migration.Migrate16xTo220IT; import org.apache.unomi.itests.graphql.*; +import org.apache.unomi.itests.migration.Migrate16xTo220IT; import org.apache.unomi.itests.migration.MigrationIT; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -35,7 +35,7 @@ import org.junit.runners.Suite.SuiteClasses; MigrationIT.class, BasicIT.class, ConditionEvaluatorIT.class, - ConditionESQueryBuilderIT.class, + ConditionQueryBuilderIT.class, SegmentIT.class, ProfileServiceIT.class, ProfileImportBasicIT.class, 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 8e2bece4a..b598195ce 100644 --- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java @@ -293,10 +293,17 @@ public abstract class BaseIT extends KarafTestSupport { 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.graphql.feature.activated", "true"), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.cluster.name", "contextElasticSearchITests"), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.addresses", "localhost:9400"), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.taskWaitingPollingInterval", "50"), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.cluster.name", "contextElasticSearchITests"), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.addresses", "localhost:9400"), + 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"), systemProperty("org.ops4j.pax.exam.rbc.rmi.port").value("1199"), systemProperty("org.apache.unomi.hazelcast.group.name").value("cellar"), diff --git a/itests/src/test/java/org/apache/unomi/itests/ConditionESQueryBuilderIT.java b/itests/src/test/java/org/apache/unomi/itests/ConditionQueryBuilderIT.java similarity index 95% rename from itests/src/test/java/org/apache/unomi/itests/ConditionESQueryBuilderIT.java rename to itests/src/test/java/org/apache/unomi/itests/ConditionQueryBuilderIT.java index 06e57f484..882364db4 100644 --- a/itests/src/test/java/org/apache/unomi/itests/ConditionESQueryBuilderIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/ConditionQueryBuilderIT.java @@ -30,13 +30,13 @@ import org.ops4j.pax.exam.spi.reactors.PerSuite; import java.util.List; /** - * Integration tests for various condition query builder types (elasticsearch). + * Integration tests for various condition query builder types (ElasticSearch or OpenSearch). * * @author Sergiy Shyrkov */ @RunWith(PaxExam.class) @ExamReactorStrategy(PerSuite.class) -public class ConditionESQueryBuilderIT extends ConditionEvaluatorIT { +public class ConditionQueryBuilderIT extends ConditionEvaluatorIT { @Override protected boolean eval(Condition c) { 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 2ef00efa8..48d48e578 100644 --- a/itests/src/test/java/org/apache/unomi/itests/HealthCheckIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/HealthCheckIT.java @@ -17,78 +17,26 @@ package org.apache.unomi.itests; -import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule; -import org.apache.commons.io.IOUtils; -import org.apache.cxf.interceptor.security.AccessDeniedException; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.config.Registry; -import org.apache.http.config.RegistryBuilder; -import org.apache.http.conn.socket.ConnectionSocketFactory; -import org.apache.http.conn.socket.PlainConnectionSocketFactory; -import org.apache.http.conn.ssl.SSLConnectionSocketFactory; -import org.apache.http.entity.ContentType; import org.apache.http.impl.client.BasicCredentialsProvider; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.client.HttpClients; -import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.apache.karaf.itests.KarafTestSupport; -import org.apache.unomi.api.services.DefinitionsService; -import org.apache.unomi.api.services.EventService; -import org.apache.unomi.api.services.ProfileService; -import org.apache.unomi.lifecycle.BundleWatcher; -import org.apache.unomi.persistence.spi.PersistenceService; -import org.junit.After; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.ops4j.pax.exam.Configuration; -import org.ops4j.pax.exam.CoreOptions; -import org.ops4j.pax.exam.Option; import org.ops4j.pax.exam.junit.PaxExam; -import org.ops4j.pax.exam.karaf.options.LogLevelOption.LogLevel; -import org.ops4j.pax.exam.options.MavenArtifactUrlReference; -import org.ops4j.pax.exam.options.extra.VMOption; import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; import org.ops4j.pax.exam.spi.reactors.PerSuite; -import org.ops4j.pax.exam.util.Filter; -import org.osgi.service.cm.ConfigurationAdmin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.inject.Inject; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; -import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.stream.Stream; import static org.junit.Assert.fail; -import static org.ops4j.pax.exam.CoreOptions.systemProperty; -import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.*; /** * Health Check Integration Tests @@ -110,7 +58,7 @@ public class HealthCheckIT extends BaseIT { LOGGER.info("health check response: {}", 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("elasticsearch") && 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("cluster") && r.getStatus() == HealthCheckResponse.Status.LIVE)); Assert.assertTrue(response.stream().anyMatch(r -> r.getName().equals("persistence") && r.getStatus() == HealthCheckResponse.Status.LIVE)); 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 623904938..3313a753d 100644 --- a/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/ProfileServiceIT.java @@ -154,21 +154,21 @@ public class ProfileServiceIT extends BaseIT { public void testGetProfileWithWrongScrollerIdThrowException() throws InterruptedException, NoSuchFieldException, IllegalAccessException, IOException { boolean throwExceptionCurrent = false; - Configuration elasticSearchConfiguration = configurationAdmin.getConfiguration("org.apache.unomi.persistence.elasticsearch"); - if (elasticSearchConfiguration != null && elasticSearchConfiguration.getProperties().get("throwExceptions") != null) { + Configuration searchEngineConfiguration = configurationAdmin.getConfiguration("org.apache.unomi.persistence." + searchEngine); + if (searchEngineConfiguration != null && searchEngineConfiguration.getProperties().get("throwExceptions") != null) { try { - if (elasticSearchConfiguration.getProperties().get("throwExceptions") instanceof String) { - throwExceptionCurrent = Boolean.parseBoolean((String) elasticSearchConfiguration.getProperties().get("throwExceptions")); + if (searchEngineConfiguration.getProperties().get("throwExceptions") instanceof String) { + throwExceptionCurrent = Boolean.parseBoolean((String) searchEngineConfiguration.getProperties().get("throwExceptions")); } else { // already a boolean - throwExceptionCurrent = (Boolean) elasticSearchConfiguration.getProperties().get("throwExceptions"); + throwExceptionCurrent = (Boolean) searchEngineConfiguration.getProperties().get("throwExceptions"); } } catch (Throwable e) { // Not able to cast the property } } - updateConfiguration(PersistenceService.class.getName(), "org.apache.unomi.persistence.elasticsearch", "throwExceptions", true); + updateConfiguration(PersistenceService.class.getName(), "org.apache.unomi.persistence." + searchEngine, "throwExceptions", true); Query query = new Query(); query.setLimit(2); @@ -181,7 +181,7 @@ public class ProfileServiceIT extends BaseIT { } catch (RuntimeException ex) { // Should get here since this scenario should throw exception } finally { - updateConfiguration(PersistenceService.class.getName(), "org.apache.unomi.persistence.elasticsearch", "throwExceptions", + updateConfiguration(PersistenceService.class.getName(), "org.apache.unomi.persistence." + searchEngine, "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 c6d4daeac..6f2b375cb 100644 --- a/itests/src/test/java/org/apache/unomi/itests/ProfileServiceWithoutOverwriteIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/ProfileServiceWithoutOverwriteIT.java @@ -44,10 +44,14 @@ public class ProfileServiceWithoutOverwriteIT extends BaseIT { @Configuration public Option[] config() { + + searchEngine = System.getProperty(SEARCH_ENGINE_PROPERTY, SEARCH_ENGINE_ELASTICSEARCH); + System.out.println("Search Engine: " + searchEngine); + List<Option> options = new ArrayList<>(); options.addAll(Arrays.asList(super.config())); - options.add(systemProperty("org.apache.unomi.elasticsearch.throwExceptions").value("true")); - options.add(systemProperty("org.apache.unomi.elasticsearch.alwaysOverwrite").value("false")); + options.add(systemProperty("org.apache.unomi."+searchEngine+".throwExceptions").value("true")); + options.add(systemProperty("org.apache.unomi."+searchEngine+".alwaysOverwrite").value("false")); return options.toArray(new Option[0]); } diff --git a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xTo220IT.java b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xTo220IT.java index 316b0bc28..f83717ee1 100644 --- a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xTo220IT.java +++ b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xTo220IT.java @@ -48,9 +48,21 @@ public class Migrate16xTo220IT extends BaseIT { "context-userlist", "context-propertytype", "context-scope", "context-conditiontype", "context-rule", "context-scoring", "context-segment", "context-groovyaction", "context-topic", "context-patch", "context-jsonschema", "context-importconfig", "context-exportconfig", "context-rulestats"); + public void checkSearchEngine() { + searchEngine = System.getProperty(SEARCH_ENGINE_PROPERTY, SEARCH_ENGINE_ELASTICSEARCH); + System.out.println("Check search engine: " + searchEngine); + } + @Override @Before public void waitForStartup() throws InterruptedException { + checkSearchEngine(); + + if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) { + System.out.println("Migration from 1.x to 2.x not supported for OpenSearch, skipping snapshot restore"); + super.waitForStartup(); + return; + } System.out.println("Restoring snapshot into search engine..."); LOGGER.info("Restoring snapshot into search engine..."); @@ -117,6 +129,10 @@ public class Migrate16xTo220IT 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; + } checkMergedProfilesAliases(); checkProfileInterests(); checkScopeHaveBeenCreated(); @@ -420,4 +436,5 @@ public class Migrate16xTo220IT extends BaseIT { Assert.assertEquals("eventTriggeredabcdefgh", pastEvents.get(0).get("key")); Assert.assertEquals(5, (int) pastEvents.get(0).get("count")); } + } diff --git a/itests/src/test/resources/org.apache.unomi.healthcheck.cfg b/itests/src/test/resources/org.apache.unomi.healthcheck.cfg index 710876fe4..dadf50dd7 100644 --- a/itests/src/test/resources/org.apache.unomi.healthcheck.cfg +++ b/itests/src/test/resources/org.apache.unomi.healthcheck.cfg @@ -22,10 +22,17 @@ esLogin = ${org.apache.unomi.elasticsearch.username:-} esPassword = ${org.apache.unomi.elasticsearch.password:-} httpClient.trustAllCertificates = ${org.apache.unomi.elasticsearch.sslTrustAllCertificates:-false} +# OpenSearch configuration +osAddresses = ${org.apache.unomi.opensearch.addresses:-localhost:9200} +osSSLEnabled = ${org.apache.unomi.opensearch.sslEnable:-false} +osLogin = ${org.apache.unomi.opensearch.username:-admin} +osPassword = ${org.apache.unomi.opensearch.password:-} +httpClient.trustAllCertificates = ${org.apache.unomi.opensearch.sslTrustAllCertificates:-false} + # Security configuration authentication.realm = ${org.apache.unomi.security.realm:-karaf} # Health check configuration healthcheck.enabled = ${org.apache.unomi.healthcheck.enabled:-true} -healthcheck.providers = ${org.apache.unomi.healthcheck.providers:-cluster,elasticsearch,unomi,persistence} +healthcheck.providers = ${org.apache.unomi.healthcheck.providers:-cluster,elasticsearch,opensearch,unomi,persistence} healthcheck.timeout = ${org.apache.unomi.healthcheck.timeout:-400} diff --git a/lifecycle-watcher/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/lifecycle-watcher/src/main/resources/OSGI-INF/blueprint/blueprint.xml index 9fceda381..25c371b6c 100644 --- a/lifecycle-watcher/src/main/resources/OSGI-INF/blueprint/blueprint.xml +++ b/lifecycle-watcher/src/main/resources/OSGI-INF/blueprint/blueprint.xml @@ -41,7 +41,6 @@ <entry key="org.apache.unomi.scripting" value="false"/> <entry key="org.apache.unomi.metrics" value="false"/> <entry key="org.apache.unomi.persistence-spi" value="false"/> - <entry key="org.apache.unomi.persistence-elasticsearch-core" value="false"/> <entry key="org.apache.unomi.services" value="false"/> <entry key="org.apache.unomi.cxs-lists-extension-services" value="false"/> <entry key="org.apache.unomi.cxs-lists-extension-rest" value="false"/> diff --git a/persistence-opensearch/conditions/src/main/java/org/apache/unomi/persistence/opensearch/conditions/PastEventConditionOSQueryBuilder.java b/persistence-opensearch/conditions/src/main/java/org/apache/unomi/persistence/opensearch/conditions/PastEventConditionOSQueryBuilder.java index fb6e0d9e6..28d558ba8 100644 --- a/persistence-opensearch/conditions/src/main/java/org/apache/unomi/persistence/opensearch/conditions/PastEventConditionOSQueryBuilder.java +++ b/persistence-opensearch/conditions/src/main/java/org/apache/unomi/persistence/opensearch/conditions/PastEventConditionOSQueryBuilder.java @@ -20,9 +20,9 @@ package org.apache.unomi.persistence.opensearch.conditions; import org.apache.unomi.api.Event; import org.apache.unomi.api.Profile; import org.apache.unomi.api.conditions.Condition; -import org.apache.unomi.api.conditions.ConditionType; import org.apache.unomi.api.services.DefinitionsService; import org.apache.unomi.api.services.SegmentService; +import org.apache.unomi.api.utils.ConditionBuilder; import org.apache.unomi.persistence.opensearch.ConditionOSQueryBuilder; import org.apache.unomi.persistence.opensearch.ConditionOSQueryBuilderDispatcher; import org.apache.unomi.persistence.spi.PersistenceService; @@ -89,7 +89,8 @@ public class PastEventConditionOSQueryBuilder implements ConditionOSQueryBuilder // TODO see for deprecation, this should not happen anymore each past event condition should have a generatedPropertyKey Condition eventCondition = getEventCondition(condition, context, null, definitionsService, scriptExecutor); Set<String> ids = getProfileIdsMatchingEventCount(eventCondition, minimumEventCount, maximumEventCount); - return dispatcher.buildFilter(getProfileIdsCondition(ids, eventsOccurred), context); + ConditionBuilder conditionBuilder = definitionsService.getConditionBuilder(); + return dispatcher.buildFilter(conditionBuilder.condition("idsCondition").parameter("ids", ids).parameter("match", eventsOccurred).build(), context); } } @@ -112,7 +113,8 @@ public class PastEventConditionOSQueryBuilder implements ConditionOSQueryBuilder } Set<String> profileIds = getProfileIdsMatchingEventCount(eventCondition, minimumEventCount, maximumEventCount); - return eventsOccurred ? profileIds.size() : persistenceService.queryCount(getProfileIdsCondition(profileIds, false), Profile.ITEM_TYPE); + ConditionBuilder conditionBuilder = definitionsService.getConditionBuilder(); + return eventsOccurred ? profileIds.size() : persistenceService.queryCount(conditionBuilder.condition("idsCondition").parameter("ids", profileIds).parameter("match", false).build(), Profile.ITEM_TYPE); } } @@ -123,44 +125,38 @@ public class PastEventConditionOSQueryBuilder implements ConditionOSQueryBuilder return operator == null || operator.equals("eventsOccurred"); } - private Condition getProfileIdsCondition(Set<String> ids, boolean shouldMatch) { - Condition idsCondition = new Condition(); - idsCondition.setConditionType(definitionsService.getConditionType("idsCondition")); - idsCondition.setParameter("ids", ids); - idsCondition.setParameter("match", shouldMatch); - return idsCondition; - } - private Condition getProfileConditionForCounter(String generatedPropertyKey, Integer minimumEventCount, Integer maximumEventCount, boolean eventsOccurred) { - String generatedPropertyName = "systemProperties.pastEvents." + generatedPropertyKey; - ConditionType profilePropertyConditionType = definitionsService.getConditionType("profilePropertyCondition"); if (eventsOccurred) { - Condition counterIsBetweenBoundaries = new Condition(); - counterIsBetweenBoundaries.setConditionType(profilePropertyConditionType); - counterIsBetweenBoundaries.setParameter("propertyName", generatedPropertyName); - counterIsBetweenBoundaries.setParameter("comparisonOperator", "between"); - counterIsBetweenBoundaries.setParameter("propertyValuesInteger", Arrays.asList(minimumEventCount, maximumEventCount)); - return counterIsBetweenBoundaries; + return createEventOccurredCondition(generatedPropertyKey, minimumEventCount, maximumEventCount); } else { - Condition counterMissing = new Condition(); - counterMissing.setConditionType(profilePropertyConditionType); - counterMissing.setParameter("propertyName", generatedPropertyName); - counterMissing.setParameter("comparisonOperator", "missing"); - - Condition counterZero = new Condition(); - counterZero.setConditionType(profilePropertyConditionType); - counterZero.setParameter("propertyName", generatedPropertyName); - counterZero.setParameter("comparisonOperator", "equals"); - counterZero.setParameter("propertyValueInteger", 0); - - Condition counterCondition = new Condition(); - counterCondition.setConditionType(definitionsService.getConditionType("booleanCondition")); - counterCondition.setParameter("operator", "or"); - counterCondition.setParameter("subConditions", Arrays.asList(counterMissing, counterZero)); - return counterCondition; + return createEventNotOccurredCondition(generatedPropertyKey); } } + private Condition createEventOccurredCondition(String generatedPropertyKey, Integer minimumEventCount, Integer maximumEventCount) { + ConditionBuilder conditionBuilder = definitionsService.getConditionBuilder(); + ConditionBuilder.ConditionItem subConditionCount = conditionBuilder.profileProperty("systemProperties.pastEvents.count").between(minimumEventCount, maximumEventCount); + ConditionBuilder.ConditionItem subConditionKey = conditionBuilder.profileProperty("systemProperties.pastEvents.key").equalTo(generatedPropertyKey); + ConditionBuilder.ConditionItem booleanCondition = conditionBuilder.and(subConditionCount, subConditionKey); + return conditionBuilder.nested(booleanCondition, "systemProperties.pastEvents").build(); + } + + private Condition createEventNotOccurredCondition(String generatedPropertyKey) { + ConditionBuilder.ConditionItem counterMissing = createPastEventMustNotExistCondition(generatedPropertyKey); + ConditionBuilder conditionBuilder = definitionsService.getConditionBuilder(); + ConditionBuilder.ConditionItem counterZero = conditionBuilder.profileProperty("systemProperties.pastEvents.count").equalTo(0); + ConditionBuilder.ConditionItem keyEquals = conditionBuilder.profileProperty("systemProperties.pastEvents.key").equalTo(generatedPropertyKey); + ConditionBuilder.ConditionItem keyExistsAndCounterZero = conditionBuilder.and(counterZero, keyEquals); + ConditionBuilder.ConditionItem nestedKeyExistsAndCounterZero = conditionBuilder.nested(keyExistsAndCounterZero, "systemProperties.pastEvents"); + return conditionBuilder.or(counterMissing, nestedKeyExistsAndCounterZero).build(); + } + + private ConditionBuilder.ConditionItem createPastEventMustNotExistCondition(String generatedPropertyKey) { + ConditionBuilder conditionBuilder = definitionsService.getConditionBuilder(); + ConditionBuilder.ConditionItem keyEquals = conditionBuilder.profileProperty("systemProperties.pastEvents.key").equalTo(generatedPropertyKey); + return conditionBuilder.not(keyEquals); + } + private Set<String> getProfileIdsMatchingEventCount(Condition eventCondition, int minimumEventCount, int maximumEventCount) { boolean noBoundaries = minimumEventCount == 1 && maximumEventCount == Integer.MAX_VALUE; if (pastEventsDisablePartitions) { diff --git a/persistence-opensearch/core/src/main/resources/META-INF/cxs/mappings/profile.json b/persistence-opensearch/core/src/main/resources/META-INF/cxs/mappings/profile.json index 81cc14d0f..6e650a178 100644 --- a/persistence-opensearch/core/src/main/resources/META-INF/cxs/mappings/profile.json +++ b/persistence-opensearch/core/src/main/resources/META-INF/cxs/mappings/profile.json @@ -40,6 +40,13 @@ } } }, + "systemProperties": { + "properties": { + "pastEvents": { + "type": "nested" + } + } + }, "consents": { "properties": { "statusDate": { diff --git a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/DateUtils.java b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/DateUtils.java index eac3e358b..0268c2212 100644 --- a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/DateUtils.java +++ b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/DateUtils.java @@ -45,7 +45,7 @@ public class DateUtils { return Date.from(instant); } catch (DateMathParseException e) { LOGGER.warn("unable to parse date. See debug log level for full stacktrace"); - LOGGER.debug("unable to parse date {}", value, e); + LOGGER.warn("unable to parse date {}", value, e); } return null; } diff --git a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/datemath/DateMathParser.java b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/datemath/DateMathParser.java index 95feb6994..4c599c1c9 100644 --- a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/datemath/DateMathParser.java +++ b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/datemath/DateMathParser.java @@ -75,7 +75,18 @@ public class DateMathParser { this.roundUpFormatter = roundUpFormatter; } + private String normalizeDateMathInput(String input) { + // Replace 't' with 'T' only when it's part of an ISO datetime format (e.g., `2022-05-18t15:23:17z`) + input = input.replaceAll("(?<=\\d{4}-\\d{2}-\\d{2})t", "T"); // Match 't' after a full date + // Replace 'z' with 'Z' only when it's at the end of the string or follows time components + input = input.replaceAll("z$", "Z"); // Match 'z' at the end + input = input.replaceAll("(?<=[:\\d])z", "Z"); // Match 'z' after a time component + return input; + } + public Instant parse(String text, LongSupplier now, boolean roundUpProperty, ZoneId timeZone) { + text = text.trim(); + Instant time; String mathString; if (text.startsWith("now")) { @@ -89,10 +100,12 @@ public class DateMathParser { int index = text.indexOf("||"); if (index == -1) { // no math, just parse date + // Normalize input for case-insensitive ISO datetime handling + text = normalizeDateMathInput(text); return parseDateTime(text, timeZone, roundUpProperty); } - time = parseDateTime(text.substring(0, index), timeZone, false); - mathString = text.substring(index + 2); + time = parseDateTime(normalizeDateMathInput(text.substring(0, index).trim()), timeZone, false); + mathString = text.substring(index + 2).trim(); } return parseMath(mathString, time, roundUpProperty, timeZone); diff --git a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/datemath/JavaDateFormatter.java b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/datemath/JavaDateFormatter.java index ab2616bf1..bc21e3908 100644 --- a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/datemath/JavaDateFormatter.java +++ b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/datemath/JavaDateFormatter.java @@ -53,6 +53,15 @@ public class JavaDateFormatter { this.allowEpochSecond = epochSecond; } + private String adjustForCaseInsensitive(String input) { + // Replace 't' with 'T' only when it's part of an ISO datetime format (e.g., `2022-05-18t15:23:17z`) + input = input.replaceAll("(?<=\\d{4}-\\d{2}-\\d{2})t", "T"); // Match 't' after a full date + // Replace 'z' with 'Z' only when it's at the end of the string or follows time components + input = input.replaceAll("z$", "Z"); // Match 'z' at the end + input = input.replaceAll("(?<=[:\\d])z", "Z"); // Match 'z' after a time component + return input; + } + public TemporalAccessor parse(String input) { // Numeric check if (isNumeric(input)) { @@ -65,6 +74,8 @@ public class JavaDateFormatter { } } + input = adjustForCaseInsensitive(input); + for (FormatDefinition def : formats) { try { String adjusted = adjustForPattern(input, def.pattern); @@ -303,6 +314,7 @@ public class JavaDateFormatter { private FormatDefinition fmt(String name, String pattern) { // Apply UTC zone to all and consider using strict resolver if needed DateTimeFormatter dtf = new DateTimeFormatterBuilder() + .parseCaseSensitive() .appendPattern(pattern) .toFormatter() .withZone(ZoneOffset.UTC); diff --git a/persistence-spi/src/test/java/org/apache/unomi/persistence/spi/conditions/datemath/DateMathParserTest.java b/persistence-spi/src/test/java/org/apache/unomi/persistence/spi/conditions/datemath/DateMathParserTest.java index 7882a3bff..348523a59 100644 --- a/persistence-spi/src/test/java/org/apache/unomi/persistence/spi/conditions/datemath/DateMathParserTest.java +++ b/persistence-spi/src/test/java/org/apache/unomi/persistence/spi/conditions/datemath/DateMathParserTest.java @@ -189,4 +189,54 @@ public class DateMathParserTest { assertTrue(e.getMessage().startsWith("failed to parse date field [not-a-date] with format")); } } + + @Test + public void testInvalidLowercaseMathOperator() { + try { + parser.parse("now*1d", fixedNow, false, ZoneOffset.UTC); // Invalid operator + fail("Expected an exception"); + } catch (DateMathParseException e) { + assertEquals("operator not supported for date math [*1d]", e.getMessage()); + } + } + + + @Test + public void testDateMathWithCaseInsensitiveParsing() { + Instant parsed = parser.parse("2001-01-01t12:00:00z||+1d", fixedNow, false, ZoneOffset.UTC); + assertEquals("2001-01-02T12:00:00Z", parsed.toString()); + + parsed = parser.parse("now+1h/d", fixedNow, false, ZoneOffset.UTC); + assertEquals("2001-01-01T00:00:00Z", parsed.toString()); + } + + @Test + public void testMixedCaseDateMath() { + Instant parsed = parser.parse("2001-01-01T12:00:00z||+1M/d", fixedNow, true, ZoneOffset.UTC); // Mixed case + assertEquals("2001-02-01T23:59:59.999Z", parsed.toString()); + } + + @Test + public void testInvalidMathWithCaseInsensitiveInput() { + try { + parser.parse("now*1d", fixedNow, false, ZoneOffset.UTC); // Invalid operator + fail("Expected an exception"); + } catch (DateMathParseException e) { + assertEquals("operator not supported for date math [*1d]", e.getMessage()); + } + + try { + parser.parse("2001-01-01t12:00:00x||+1d", fixedNow, false, ZoneOffset.UTC); // Invalid separator + fail("Expected an exception"); + } catch (DateMathParseException e) { + assertTrue(e.getMessage().contains("failed to parse date field")); + } + } + + @Test + public void testDateMathWithExtraSpaces() { + Instant parsed = parser.parse(" 2001-01-01T12:00:00Z || +1d ", fixedNow, false, ZoneOffset.UTC); + assertEquals("2001-01-02T12:00:00Z", parsed.toString()); + } + } diff --git a/persistence-spi/src/test/java/org/apache/unomi/persistence/spi/conditions/datemath/JavaDateFormatterTest.java b/persistence-spi/src/test/java/org/apache/unomi/persistence/spi/conditions/datemath/JavaDateFormatterTest.java index 76a52c79e..e43ad7c30 100644 --- a/persistence-spi/src/test/java/org/apache/unomi/persistence/spi/conditions/datemath/JavaDateFormatterTest.java +++ b/persistence-spi/src/test/java/org/apache/unomi/persistence/spi/conditions/datemath/JavaDateFormatterTest.java @@ -159,6 +159,45 @@ public class JavaDateFormatterTest { } } - // Add more tests as needed for strict variants, time_no_millis, t_time, week_date, etc. - // The provided tests give a broad coverage of different formats. + @Test + public void testMixedCaseDate() { + JavaDateFormatter formatter = new JavaDateFormatter("strict_date_optional_time"); + Instant parsed = Instant.from(formatter.parse("2022-05-18T15:23:17z")); // mixed case 'T' and 'z' + assertEquals("2022-05-18T15:23:17Z", parsed.toString()); + } + + @Test + public void testCaseInsensitiveISOWithValidInputs() { + JavaDateFormatter formatter = new JavaDateFormatter("strict_date_optional_time"); + // Lowercase `t` and `z` + Instant parsed = Instant.from(formatter.parse("2022-05-18t15:23:17z")); + assertEquals("2022-05-18T15:23:17Z", parsed.toString()); + + // Mixed case + parsed = Instant.from(formatter.parse("2022-05-18T15:23:17z")); + assertEquals("2022-05-18T15:23:17Z", parsed.toString()); + + // Uppercase (valid) + parsed = Instant.from(formatter.parse("2022-05-18T15:23:17Z")); + assertEquals("2022-05-18T15:23:17Z", parsed.toString()); + } + + @Test + public void testCaseInsensitiveISOWithInvalidInputs() { + JavaDateFormatter formatter = new JavaDateFormatter("strict_date_optional_time"); + + try { + formatter.parse("2022-05-18x15:23:17z"); // Invalid separator + fail("Expected an exception"); + } catch (DateMathParseException e) { + assertTrue(e.getMessage().contains("failed to parse date field")); + } + + try { + formatter.parse("2022-05-18T15:23:17X"); // Invalid character for timezone + fail("Expected an exception"); + } catch (DateMathParseException e) { + assertTrue(e.getMessage().contains("failed to parse date field")); + } + } }